chore: update readme (#98)

This commit is contained in:
Nathan Flurry 2026-02-06 03:03:24 -08:00 committed by GitHub
parent c0800e1a43
commit dc2a2b1687
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 379 additions and 136 deletions

View file

@ -1,17 +0,0 @@
[package]
name = "gigacode"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
description = "Sandbox Agent CLI with OpenCode attach by default"
repository.workspace = true
[[bin]]
name = "gigacode"
path = "src/main.rs"
[dependencies]
clap.workspace = true
sandbox-agent.workspace = true
tracing.workspace = true

View file

@ -1,80 +0,0 @@
# GigaCode
Use [OpenCode](https://opencode.ai)'s UI with any coding agent.
Supports Claude Code, Codex, and Amp.
This is __not__ a fork. It's powered by [Sandbox Agent SDK](https://sandboxagent.dev)'s wizardry.
> **Experimental**: This project is under active development. Please report bugs on [GitHub Issues](https://github.com/rivet-dev/sandbox-agent/issues) or join our [Discord](https://rivet.dev/discord).
## How It Works
```
┌─ GigaCode ────────────────────────────────────────────────────────┐
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ OpenCode TUI │───▶│ Sandbox Agent │───▶│ Claude Code / │ │
│ │ │ │ │ │ Codex / Amp │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
```
- [Sandbox Agent SDK](https://sandboxagent.dev) provides a universal HTTP API for controlling Claude Code, Codex, and Amp
- Sandbox Agent SDK exposes an [OpenCode-compatible endpoint](https://sandboxagent.dev/opencode-compatibility) so OpenCode can talk to any agent
- OpenCode connects to Sandbox Agent SDK via [`attach`](https://opencode.ai/docs/cli/#attach)
## Install
**macOS / Linux / WSL (Recommended)**
```bash
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/gigacode-install.sh | sh
```
**npm i -g**
```bash
npm install -g gigacode
gigacode --help
```
**bun add -g**
```bash
bun add -g gigacode
# Allow Bun to run postinstall scripts for native binaries.
bun pm -g trust gigacode-linux-x64 gigacode-linux-arm64 gigacode-darwin-arm64 gigacode-darwin-x64 gigacode-win32-x64
gigacode --help
```
**npx**
```bash
npx gigacode --help
```
**bunx**
```bash
bunx gigacode --help
```
> **Note:** Windows is unsupported. Please use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
## Usage
**TUI**
Launch the OpenCode TUI with any coding agent:
```bash
gigacode
```
**Web UI**
Use the [OpenCode Web UI](https://sandboxagent.dev/opencode-compatibility) to control any coding agent from the browser.
**OpenCode SDK**
Use the [`@opencode-ai/sdk`](https://sandboxagent.dev/opencode-compatibility) to programmatically control any coding agent.

View file

@ -1,28 +0,0 @@
use clap::Parser;
use sandbox_agent::cli::{
CliConfig, CliError, Command, GigacodeCli, OpencodeArgs, init_logging, run_command,
};
fn main() {
if let Err(err) = run() {
tracing::error!(error = %err, "gigacode failed");
std::process::exit(1);
}
}
fn run() -> Result<(), CliError> {
let cli = GigacodeCli::parse();
let config = CliConfig {
token: cli.token,
no_token: cli.no_token,
gigacode: true,
};
let command = cli
.command
.unwrap_or_else(|| Command::Opencode(OpencodeArgs::default()));
if let Err(err) = init_logging(&command) {
eprintln!("failed to init logging: {err}");
return Err(err);
}
run_command(&command, &config)
}

View file

@ -580,7 +580,7 @@ fn run_api(command: &ApiCommand, cli: &CliConfig) -> Result<(), CliError> {
}
fn run_opencode(cli: &CliConfig, args: &OpencodeArgs) -> Result<(), CliError> {
let name = if cli.gigacode { "GigaCode" } else { "OpenCode command" };
let name = if cli.gigacode { "Gigacode" } else { "OpenCode command" };
write_stderr_line(&format!("EXPERIMENTAL: Please report bugs to:\n- GitHub: https://github.com/rivet-dev/sandbox-agent/issues\n- Discord: https://rivet.dev/discord\n\n{name} is powered by:- OpenCode (TUI): https://opencode.ai/\n- Sandbox Agent SDK (multi-agent compatibility): https://sandboxagent.dev/\n\n"))?;
let token = cli.token.clone();

View file

@ -4,7 +4,7 @@
//! stubbed responses with deterministic helpers for snapshot testing. A minimal
//! in-memory state tracks sessions/messages/ptys to keep behavior coherent.
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::convert::Infallible;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
@ -37,9 +37,8 @@ static MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1);
static PART_COUNTER: AtomicU64 = AtomicU64::new(1);
static PTY_COUNTER: AtomicU64 = AtomicU64::new(1);
static PROJECT_COUNTER: AtomicU64 = AtomicU64::new(1);
const OPENCODE_PROVIDER_ID: &str = "sandbox-agent";
const OPENCODE_PROVIDER_NAME: &str = "Sandbox Agent";
const OPENCODE_DEFAULT_MODEL_ID: &str = "mock";
const OPENCODE_DEFAULT_PROVIDER_ID: &str = "mock";
const OPENCODE_DEFAULT_AGENT_MODE: &str = "build";
#[derive(Clone, Debug)]
@ -220,14 +219,19 @@ struct OpenCodeSessionRuntime {
#[derive(Clone, Debug)]
struct OpenCodeModelEntry {
agent: AgentId,
model: AgentModelInfo,
group_id: String,
group_name: String,
}
#[derive(Clone, Debug)]
struct OpenCodeModelCache {
entries: Vec<OpenCodeModelEntry>,
model_lookup: HashMap<String, AgentId>,
group_defaults: HashMap<String, String>,
group_agents: HashMap<String, AgentId>,
group_names: HashMap<String, String>,
default_group: String,
default_model: String,
}
@ -390,13 +394,17 @@ async fn ensure_backing_session(
state: &Arc<OpenCodeAppState>,
session_id: &str,
agent: &str,
model: Option<String>,
variant: Option<String>,
) -> Result<(), SandboxError> {
let model = model.filter(|value| !value.trim().is_empty());
let variant = variant.filter(|value| !value.trim().is_empty());
let request = CreateSessionRequest {
agent: agent.to_string(),
agent_mode: None,
permission_mode: None,
model: None,
variant: None,
model: model.clone(),
variant: variant.clone(),
agent_version: None,
};
match state
@ -406,7 +414,17 @@ async fn ensure_backing_session(
.await
{
Ok(_) => Ok(()),
Err(SandboxError::SessionAlreadyExists { .. }) => Ok(()),
Err(SandboxError::SessionAlreadyExists { .. }) => {
state
.inner
.session_manager()
.set_session_overrides(session_id, model, variant)
.await
.or_else(|err| match err {
SandboxError::SessionNotFound { .. } => Ok(()),
other => Err(other),
})
}
Err(err) => Err(err),
}
}
@ -623,6 +641,10 @@ async fn opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
let mut entries = Vec::new();
let mut model_lookup = HashMap::new();
let mut ambiguous_models = HashSet::new();
let mut group_defaults: HashMap<String, String> = HashMap::new();
let mut group_agents: HashMap<String, AgentId> = HashMap::new();
let mut group_names: HashMap<String, String> = HashMap::new();
let mut default_model: Option<String> = None;
for agent in available_agent_ids() {
@ -631,32 +653,97 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
Err(_) => continue,
};
if default_model.is_none() {
default_model = response
.default_model
.clone()
.or_else(|| response.models.first().map(|model| model.id.clone()));
}
let first_model_id = response.models.first().map(|model| model.id.clone());
for model in response.models {
let model_id = model.id.clone();
if model_lookup.contains_key(&model_id) {
continue;
let (group_id, group_name) = group_for_agent_model(agent, &model_id);
if response.default_model.as_deref() == Some(model_id.as_str()) {
group_defaults.insert(group_id.clone(), model_id.clone());
}
model_lookup.insert(model_id.clone(), agent);
entries.push(OpenCodeModelEntry { agent, model });
group_agents.entry(group_id.clone()).or_insert(agent);
group_names
.entry(group_id.clone())
.or_insert_with(|| group_name.clone());
if !ambiguous_models.contains(&model_id) {
match model_lookup.get(&model_id) {
None => {
model_lookup.insert(model_id.clone(), agent);
}
Some(existing) if *existing != agent => {
model_lookup.remove(&model_id);
ambiguous_models.insert(model_id.clone());
}
_ => {}
}
}
entries.push(OpenCodeModelEntry {
model,
group_id,
group_name,
});
}
if default_model.is_none() {
default_model = response.default_model.clone().or(first_model_id);
}
}
let default_model = if model_lookup.contains_key(OPENCODE_DEFAULT_MODEL_ID) {
OPENCODE_DEFAULT_MODEL_ID.to_string()
} else {
default_model.unwrap_or_else(|| OPENCODE_DEFAULT_MODEL_ID.to_string())
};
let mut groups: BTreeMap<String, Vec<&OpenCodeModelEntry>> = BTreeMap::new();
for entry in &entries {
groups
.entry(entry.group_id.clone())
.or_default()
.push(entry);
}
for entries in groups.values_mut() {
entries.sort_by(|a, b| a.model.id.cmp(&b.model.id));
}
if entries
.iter()
.any(|entry| entry.model.id == OPENCODE_DEFAULT_MODEL_ID)
{
default_model = Some(OPENCODE_DEFAULT_MODEL_ID.to_string());
}
let default_model = default_model.unwrap_or_else(|| {
entries
.first()
.map(|entry| entry.model.id.clone())
.unwrap_or_else(|| OPENCODE_DEFAULT_MODEL_ID.to_string())
});
let mut default_group = entries
.iter()
.find(|entry| entry.model.id == default_model)
.map(|entry| entry.group_id.clone())
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
if !groups.contains_key(&default_group) {
if let Some((group_id, _)) = groups.iter().next() {
default_group = group_id.clone();
}
}
for (group_id, entries) in &groups {
if !group_defaults.contains_key(group_id) {
if let Some(entry) = entries.first() {
group_defaults.insert(group_id.clone(), entry.model.id.clone());
}
}
}
OpenCodeModelCache {
entries,
model_lookup,
group_defaults,
group_agents,
group_names,
default_group,
default_model,
}
}
@ -666,8 +753,8 @@ fn resolve_agent_from_model(
provider_id: &str,
model_id: &str,
) -> Option<AgentId> {
if provider_id != OPENCODE_PROVIDER_ID {
return None;
if let Some(agent) = cache.group_agents.get(provider_id) {
return Some(*agent);
}
if let Some(agent) = cache.model_lookup.get(model_id) {
return Some(*agent);
@ -675,6 +762,9 @@ fn resolve_agent_from_model(
if let Some(agent) = AgentId::parse(model_id) {
return Some(agent);
}
if opencode_group_provider(provider_id).is_some() {
return Some(AgentId::Opencode);
}
if model_id.contains('/') {
return Some(AgentId::Opencode);
}
@ -706,15 +796,31 @@ async fn resolve_session_agent(
let default_model_id = cache.default_model.clone();
let mut provider_id = requested_provider
.filter(|value| !value.is_empty())
.unwrap_or(OPENCODE_PROVIDER_ID)
.to_string();
let mut model_id = requested_model
.filter(|value| *value != "sandbox-agent")
.map(|value| value.to_string());
let model_id = requested_model
.filter(|value| !value.is_empty())
.unwrap_or(default_model_id.as_str())
.to_string();
.map(|value| value.to_string());
if provider_id.is_none() {
if let Some(model_value) = model_id.as_deref() {
if let Some(entry) = cache
.entries
.iter()
.find(|entry| entry.model.id == model_value)
{
provider_id = Some(entry.group_id.clone());
} else if let Some(agent) = AgentId::parse(model_value) {
provider_id = Some(agent.as_str().to_string());
}
}
}
let mut provider_id = provider_id.unwrap_or_else(|| cache.default_group.clone());
let mut model_id = model_id
.or_else(|| cache.group_defaults.get(&provider_id).cloned())
.unwrap_or_else(|| default_model_id.clone());
let mut resolved_agent = resolve_agent_from_model(&cache, &provider_id, &model_id);
if resolved_agent.is_none() {
provider_id = OPENCODE_PROVIDER_ID.to_string();
provider_id = cache.default_group.clone();
model_id = default_model_id.clone();
resolved_agent =
resolve_agent_from_model(&cache, &provider_id, &model_id).or_else(|| Some(default_agent_id()));
@ -756,19 +862,63 @@ fn agent_display_name(agent: AgentId) -> &'static str {
}
}
fn model_config_entry(agent: AgentId, model: &AgentModelInfo) -> Value {
let model_name = model.name.clone().unwrap_or_else(|| model.id.clone());
let variants = model_variants_object(model);
fn opencode_model_provider(model_id: &str) -> Option<&str> {
model_id.split_once('/').map(|(provider, _)| provider)
}
fn opencode_group_provider(group_id: &str) -> Option<&str> {
group_id.strip_prefix("opencode:")
}
fn group_for_agent_model(agent: AgentId, model_id: &str) -> (String, String) {
if agent == AgentId::Opencode {
let provider = opencode_model_provider(model_id).unwrap_or("unknown");
return (
format!("opencode:{provider}"),
format!("OpenCode ({provider})"),
);
}
let group_id = agent.as_str().to_string();
let group_name = agent_display_name(agent).to_string();
(group_id, group_name)
}
fn backing_model_for_agent(agent: AgentId, provider_id: &str, model_id: &str) -> Option<String> {
if model_id.trim().is_empty() {
return None;
}
if AgentId::parse(model_id).is_some() {
return None;
}
if agent != AgentId::Opencode {
return Some(model_id.to_string());
}
if model_id.contains('/') {
return Some(model_id.to_string());
}
if let Some(provider) = opencode_group_provider(provider_id) {
return Some(format!("{provider}/{model_id}"));
}
Some(model_id.to_string())
}
fn model_config_entry(entry: &OpenCodeModelEntry) -> Value {
let model_name = entry
.model
.name
.clone()
.unwrap_or_else(|| entry.model.id.clone());
let variants = model_variants_object(&entry.model);
json!({
"id": model.id,
"providerID": OPENCODE_PROVIDER_ID,
"id": entry.model.id,
"providerID": entry.group_id,
"api": {
"id": "sandbox-agent",
"url": "http://localhost",
"npm": "@sandbox-agent/sdk"
},
"name": model_name,
"family": agent_display_name(agent),
"family": entry.group_name,
"capabilities": {
"temperature": true,
"reasoning": true,
@ -807,13 +957,17 @@ fn model_config_entry(agent: AgentId, model: &AgentModelInfo) -> Value {
})
}
fn model_summary_entry(agent: AgentId, model: &AgentModelInfo) -> Value {
let model_name = model.name.clone().unwrap_or_else(|| model.id.clone());
let variants = model_variants_object(model);
fn model_summary_entry(entry: &OpenCodeModelEntry) -> Value {
let model_name = entry
.model
.name
.clone()
.unwrap_or_else(|| entry.model.id.clone());
let variants = model_variants_object(&entry.model);
json!({
"id": model.id,
"id": entry.model.id,
"name": model_name,
"family": agent_display_name(agent),
"family": entry.group_name,
"release_date": "2024-01-01",
"attachment": false,
"reasoning": true,
@ -1673,7 +1827,7 @@ async fn apply_item_event(
let provider_id = runtime
.last_model_provider
.clone()
.unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
let model_id = runtime
.last_model_id
.clone()
@ -1984,7 +2138,7 @@ async fn apply_tool_item_event(
let provider_id = runtime
.last_model_provider
.clone()
.unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
let model_id = runtime
.last_model_id
.clone()
@ -2218,7 +2372,7 @@ async fn apply_item_delta(
let provider_id = runtime
.last_model_provider
.clone()
.unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
let model_id = runtime
.last_model_id
.clone()
@ -2420,7 +2574,7 @@ pub fn build_opencode_router(state: Arc<OpenCodeAppState>) -> Router {
)]
async fn oc_agent_list(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse {
let agent = json!({
"name": OPENCODE_PROVIDER_NAME,
"name": "Sandbox Agent",
"description": "Sandbox Agent compatibility layer",
"mode": "all",
"native": false,
@ -2472,28 +2626,41 @@ async fn oc_config_providers(
State(state): State<Arc<OpenCodeAppState>>,
) -> impl IntoResponse {
let cache = opencode_model_cache(&state).await;
let mut models = serde_json::Map::new();
let mut grouped: BTreeMap<String, Vec<&OpenCodeModelEntry>> = BTreeMap::new();
for entry in &cache.entries {
models.insert(
entry.model.id.clone(),
model_config_entry(entry.agent, &entry.model),
);
grouped
.entry(entry.group_id.clone())
.or_default()
.push(entry);
}
let mut providers = Vec::new();
let mut defaults = serde_json::Map::new();
for (group_id, entries) in grouped {
let mut models = serde_json::Map::new();
for entry in entries {
models.insert(entry.model.id.clone(), model_config_entry(entry));
}
let name = cache
.group_names
.get(&group_id)
.cloned()
.unwrap_or_else(|| group_id.clone());
providers.push(json!({
"id": group_id,
"name": name,
"source": "custom",
"env": [],
"key": "",
"options": {},
"models": Value::Object(models),
}));
if let Some(default_model) = cache.group_defaults.get(&group_id) {
defaults.insert(group_id, Value::String(default_model.clone()));
}
}
let providers = json!({
"providers": [
{
"id": OPENCODE_PROVIDER_ID,
"name": OPENCODE_PROVIDER_NAME,
"source": "custom",
"env": [],
"key": "",
"options": {},
"models": Value::Object(models),
}
],
"default": {
OPENCODE_PROVIDER_ID: cache.default_model
}
"providers": providers,
"default": Value::Object(defaults),
});
(StatusCode::OK, Json(providers))
}
@ -3144,6 +3311,9 @@ async fn oc_session_message_create(
.and_then(|v| v.as_str());
let (session_agent, provider_id, model_id) =
resolve_session_agent(&state, &session_id, requested_provider, requested_model).await;
let session_agent_id = AgentId::parse(&session_agent).unwrap_or_else(default_agent_id);
let backing_model = backing_model_for_agent(session_agent_id, &provider_id, &model_id);
let backing_variant = body.variant.clone();
let parts_input = body.parts.unwrap_or_default();
if parts_input.is_empty() {
@ -3207,7 +3377,15 @@ async fn oc_session_message_create(
})
.await;
if let Err(err) = ensure_backing_session(&state, &session_id, &session_agent).await {
if let Err(err) = ensure_backing_session(
&state,
&session_id,
&session_agent,
backing_model,
backing_variant,
)
.await
{
tracing::warn!(
target = "sandbox_agent::opencode",
?err,
@ -3413,7 +3591,7 @@ async fn oc_session_command(
&directory,
&worktree,
&agent,
OPENCODE_PROVIDER_ID,
OPENCODE_DEFAULT_PROVIDER_ID,
OPENCODE_DEFAULT_MODEL_ID,
);
@ -3463,7 +3641,7 @@ async fn oc_session_shell(
.as_ref()
.and_then(|v| v.get("providerID"))
.and_then(|v| v.as_str())
.unwrap_or(OPENCODE_PROVIDER_ID),
.unwrap_or(OPENCODE_DEFAULT_PROVIDER_ID),
body.model
.as_ref()
.and_then(|v| v.get("modelID"))
@ -3775,26 +3953,41 @@ async fn oc_provider_list(
State(state): State<Arc<OpenCodeAppState>>,
) -> impl IntoResponse {
let cache = opencode_model_cache(&state).await;
let mut models = serde_json::Map::new();
let mut grouped: BTreeMap<String, Vec<&OpenCodeModelEntry>> = BTreeMap::new();
for entry in &cache.entries {
models.insert(
entry.model.id.clone(),
model_summary_entry(entry.agent, &entry.model),
);
grouped
.entry(entry.group_id.clone())
.or_default()
.push(entry);
}
let mut providers = Vec::new();
let mut defaults = serde_json::Map::new();
let mut connected = Vec::new();
for (group_id, entries) in grouped {
let mut models = serde_json::Map::new();
for entry in entries {
models.insert(entry.model.id.clone(), model_summary_entry(entry));
}
let name = cache
.group_names
.get(&group_id)
.cloned()
.unwrap_or_else(|| group_id.clone());
providers.push(json!({
"id": group_id,
"name": name,
"env": [],
"models": Value::Object(models),
}));
if let Some(default_model) = cache.group_defaults.get(&group_id) {
defaults.insert(group_id.clone(), Value::String(default_model.clone()));
}
connected.push(group_id);
}
let providers = json!({
"all": [
{
"id": OPENCODE_PROVIDER_ID,
"name": OPENCODE_PROVIDER_NAME,
"env": [],
"models": Value::Object(models),
}
],
"default": {
OPENCODE_PROVIDER_ID: cache.default_model
},
"connected": [OPENCODE_PROVIDER_ID]
"all": providers,
"default": Value::Object(defaults),
"connected": connected
});
(StatusCode::OK, Json(providers))
}
@ -3805,11 +3998,15 @@ async fn oc_provider_list(
responses((status = 200)),
tag = "opencode"
)]
async fn oc_provider_auth() -> impl IntoResponse {
let auth = json!({
OPENCODE_PROVIDER_ID: []
});
(StatusCode::OK, Json(auth))
async fn oc_provider_auth(
State(state): State<Arc<OpenCodeAppState>>,
) -> impl IntoResponse {
let cache = opencode_model_cache(&state).await;
let mut auth_map = serde_json::Map::new();
for group_id in cache.group_names.keys() {
auth_map.insert(group_id.clone(), json!([]));
}
(StatusCode::OK, Json(Value::Object(auth_map)))
}
#[utoipa::path(

View file

@ -1692,6 +1692,27 @@ impl SessionManager {
})
}
pub(crate) async fn set_session_overrides(
&self,
session_id: &str,
model: Option<String>,
variant: Option<String>,
) -> Result<(), SandboxError> {
let mut sessions = self.sessions.lock().await;
let Some(session) = SessionManager::session_mut(&mut sessions, session_id) else {
return Err(SandboxError::SessionNotFound {
session_id: session_id.to_string(),
});
};
if let Some(model) = model {
session.model = Some(model);
}
if let Some(variant) = variant {
session.variant = Some(variant);
}
Ok(())
}
async fn agent_modes(&self, agent: AgentId) -> Result<Vec<AgentModeInfo>, SandboxError> {
if agent != AgentId::Opencode {
return Ok(agent_modes_for(agent));

View file

@ -115,7 +115,7 @@ describe("OpenCode-compatible Event Streaming", () => {
await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Say hello" }],
},
});
@ -180,7 +180,7 @@ describe("OpenCode-compatible Event Streaming", () => {
await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "echo hello" }],
},
});
@ -223,7 +223,7 @@ describe("OpenCode-compatible Event Streaming", () => {
await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "tool" }],
},
});

View file

@ -45,7 +45,7 @@ describe("OpenCode-compatible Messaging API", () => {
const response = await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Hello, world!" }],
},
});
@ -58,7 +58,7 @@ describe("OpenCode-compatible Messaging API", () => {
const response = await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Say hello" }],
},
});
@ -72,7 +72,7 @@ describe("OpenCode-compatible Messaging API", () => {
const response = await client.session.promptAsync({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Process this asynchronously" }],
},
});
@ -96,7 +96,7 @@ describe("OpenCode-compatible Messaging API", () => {
await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Test message" }],
},
});
@ -116,7 +116,7 @@ describe("OpenCode-compatible Messaging API", () => {
await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Test" }],
},
});
@ -144,7 +144,7 @@ describe("OpenCode-compatible Messaging API", () => {
await client.session.promptAsync({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "Long running task" }],
},
});

View file

@ -28,22 +28,25 @@ describe("OpenCode-compatible Model API", () => {
it("should list models grouped by agent with real model IDs", async () => {
const response = await client.provider.list();
const provider = response.data?.all?.find((entry) => entry.id === "sandbox-agent");
expect(provider).toBeDefined();
const providers = response.data?.all ?? [];
const mockProvider = providers.find((entry) => entry.id === "mock");
const ampProvider = providers.find((entry) => entry.id === "amp");
const sandboxProvider = providers.find((entry) => entry.id === "sandbox-agent");
expect(sandboxProvider).toBeUndefined();
expect(mockProvider).toBeDefined();
expect(ampProvider).toBeDefined();
const models = provider?.models ?? {};
const modelIds = Object.keys(models);
expect(modelIds.length).toBeGreaterThan(0);
const mockModels = mockProvider?.models ?? {};
expect(mockModels["mock"]).toBeDefined();
expect(mockModels["mock"].id).toBe("mock");
expect(mockModels["mock"].family).toBe("Mock");
expect(models["mock"]).toBeDefined();
expect(models["mock"].id).toBe("mock");
expect(models["mock"].family).toBe("Mock");
const ampModels = ampProvider?.models ?? {};
expect(ampModels["smart"]).toBeDefined();
expect(ampModels["smart"].id).toBe("smart");
expect(ampModels["smart"].family).toBe("Amp");
expect(models["smart"]).toBeDefined();
expect(models["smart"].id).toBe("smart");
expect(models["smart"].family).toBe("Amp");
expect(models["amp"]).toBeUndefined();
expect(response.data?.default?.["sandbox-agent"]).toBe("mock");
expect(response.data?.default?.["mock"]).toBe("mock");
expect(response.data?.default?.["amp"]).toBe("smart");
});
});

View file

@ -57,7 +57,7 @@ describe("OpenCode-compatible Permission API", () => {
it("should receive permission.asked and reply via global endpoint", async () => {
await client.session.prompt({
sessionID: sessionId,
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: permissionPrompt }],
});
@ -77,7 +77,7 @@ describe("OpenCode-compatible Permission API", () => {
it("should accept permission response for a session", async () => {
await client.session.prompt({
sessionID: sessionId,
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: permissionPrompt }],
});

View file

@ -49,7 +49,7 @@ describe("OpenCode-compatible Question API", () => {
it("should ask a question and accept a reply", async () => {
await client.session.prompt({
sessionID: sessionId,
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: questionPrompt }],
});
@ -67,7 +67,7 @@ describe("OpenCode-compatible Question API", () => {
it("should allow rejecting a question", async () => {
await client.session.prompt({
sessionID: sessionId,
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: questionPrompt }],
});

View file

@ -72,7 +72,7 @@ describe("OpenCode-compatible Tool + File Actions", () => {
await client.session.prompt({
path: { id: sessionId },
body: {
model: { providerID: "sandbox-agent", modelID: "mock" },
model: { providerID: "mock", modelID: "mock" },
parts: [{ type: "text", text: "tool" }],
},
});