mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 09:01:17 +00:00
chore: update readme
This commit is contained in:
parent
9ab27ad1a8
commit
b7fa0f75a3
19 changed files with 379 additions and 136 deletions
BIN
.github/media/gigacode-header.jpeg
vendored
Normal file
BIN
.github/media/gigacode-header.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 628 KiB |
|
|
@ -83,7 +83,7 @@ SANDBOX_AGENT_SKIP_INSPECTOR=1 pnpm --filter @sandbox-agent/opencode-compat-test
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
- The product name is "GigaCode" (capital G, capital C). The CLI binary/package is `gigacode` (lowercase).
|
- The product name is "Gigacode" (capital G, lowercase c). The CLI binary/package is `gigacode` (lowercase).
|
||||||
|
|
||||||
## Git Commits
|
## Git Commits
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["server/packages/*"]
|
members = ["server/packages/*", "gigacode"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@
|
||||||
<a href="https://sandboxagent.dev/docs">Documentation</a> — <a href="https://sandboxagent.dev/docs/api-reference">API Reference</a> — <a href="https://rivet.dev/discord">Discord</a>
|
<a href="https://sandboxagent.dev/docs">Documentation</a> — <a href="https://sandboxagent.dev/docs/api-reference">API Reference</a> — <a href="https://rivet.dev/discord">Discord</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<em><strong>Experimental:</strong> <a href="./gigacode/">Gigacode</a> — use OpenCode's TUI with any coding agent.</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Why Sandbox Agent?
|
## Why Sandbox Agent?
|
||||||
|
|
||||||
Running coding agents remotely is hard. Existing SDKs assume local execution, SSH breaks TTY handling and streaming, and every agent has a different API. Building from scratch means reimplementing everything for each coding agent.
|
Running coding agents remotely is hard. Existing SDKs assume local execution, SSH breaks TTY handling and streaming, and every agent has a different API. Building from scratch means reimplementing everything for each coding agent.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: GigaCode
|
title: Gigacode
|
||||||
url: "https://github.com/rivet-dev/sandbox-agent/tree/main/server/packages/gigacode"
|
url: "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ for await (const event of events.stream) {
|
||||||
- **Authentication**: If sandbox-agent is started with `--token`, include `Authorization: Bearer <token>` header or use `--password` flag with CLI
|
- **Authentication**: If sandbox-agent is started with `--token`, include `Authorization: Bearer <token>` header or use `--password` flag with CLI
|
||||||
- **CORS**: When using the web UI from a different origin, configure `--cors-allow-origin`
|
- **CORS**: When using the web UI from a different origin, configure `--cors-allow-origin`
|
||||||
- **Provider Selection**: Use the provider/model selector in the UI to choose which backing agent to use (claude, codex, opencode, amp)
|
- **Provider Selection**: Use the provider/model selector in the UI to choose which backing agent to use (claude, codex, opencode, amp)
|
||||||
|
- **Models & Variants**: Providers are grouped by backing agent (e.g. Claude Code, Codex, Amp). OpenCode models are grouped by `OpenCode (<provider>)` to preserve their native provider grouping. Each model keeps its real model ID, and variants are exposed when available (Codex/OpenCode/Amp).
|
||||||
|
|
||||||
## Endpoint Coverage
|
## Endpoint Coverage
|
||||||
|
|
||||||
|
|
@ -132,7 +133,7 @@ See the full endpoint compatibility table below. Most endpoints are functional f
|
||||||
| `POST /permission/{id}/reply` | ✓ | Respond to permission requests |
|
| `POST /permission/{id}/reply` | ✓ | Respond to permission requests |
|
||||||
| `GET /question` | ✓ | List pending questions |
|
| `GET /question` | ✓ | List pending questions |
|
||||||
| `POST /question/{id}/reply` | ✓ | Answer agent questions |
|
| `POST /question/{id}/reply` | ✓ | Answer agent questions |
|
||||||
| `GET /provider` | − | Returns provider metadata |
|
| `GET /provider` | ✓ | Returns provider metadata |
|
||||||
| `GET /agent` | − | Returns agent list |
|
| `GET /agent` | − | Returns agent list |
|
||||||
| `GET /config` | − | Returns config |
|
| `GET /config` | − | Returns config |
|
||||||
| *other endpoints* | − | Return empty/stub responses |
|
| *other endpoints* | − | Return empty/stub responses |
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
# GigaCode
|
<p align="center">
|
||||||
|
<img src="../.github/media/gigacode-header.jpeg" alt="Gigacode. Use OpenCode's UI with any coding agent." />
|
||||||
|
</p>
|
||||||
|
|
||||||
Use [OpenCode](https://opencode.ai)'s UI with any coding agent.
|
<h3 align="center">Supports Claude Code, Codex, and Amp.</h3>
|
||||||
|
|
||||||
Supports Claude Code, Codex, and Amp.
|
<p align="center">
|
||||||
|
<i>This is <u>not</u> a fork (and never will be).<br/>It's powered by <a href="https://sandboxagent.dev">Sandbox Agent SDK</a>'s wizardry.<br/>Experimental & just for fun.</i>
|
||||||
|
</p>
|
||||||
|
|
||||||
This is __not__ a fork. It's powered by [Sandbox Agent SDK](https://sandboxagent.dev)'s wizardry.
|
<p align="center">
|
||||||
|
<a href="https://github.com/rivet-dev/sandbox-agent/issues">Issues</a> — <a href="https://rivet.dev/discord">Discord</a> — <a href="https://sandboxagent.dev/docs/opencode-compatibility#endpoint-coverage">Supported OpenCode Features</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> **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
|
## How It Works
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─ GigaCode ────────────────────────────────────────────────────────┐
|
┌─ Gigacode ────────────────────────────────────────────────────────┐
|
||||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
│ │ OpenCode TUI │───▶│ Sandbox Agent │───▶│ Claude Code / │ │
|
│ │ OpenCode TUI │───▶│ Sandbox Agent │───▶│ Claude Code / │ │
|
||||||
│ │ │ │ │ │ Codex / Amp │ │
|
│ │ │ │ │ │ Codex / Amp │ │
|
||||||
|
|
@ -20,9 +25,21 @@ This is __not__ a fork. It's powered by [Sandbox Agent SDK](https://sandboxagent
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Sandbox Agent SDK](https://sandboxagent.dev) provides a universal HTTP API for controlling Claude Code, Codex, and 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
|
- Sandbox Agent SDK exposes an [OpenCode-compatible endpoint](https://sandboxagent.dev/docs/opencode-compatibility) so OpenCode can talk to any agent
|
||||||
- OpenCode connects to Sandbox Agent SDK via [`attach`](https://opencode.ai/docs/cli/#attach)
|
- OpenCode connects to Sandbox Agent SDK via [`attach`](https://opencode.ai/docs/cli/#attach)
|
||||||
|
|
||||||
|
## OpenCode Models vs Gigacode Agents
|
||||||
|
|
||||||
|
- **OpenCode** supports **switching between inference providers** (Anthropic, OpenAI, etc.). This is OpenCode talking directly to the models with its own tools, system prompts, and agentic loop.
|
||||||
|
- **Gigacode** automates other coding agent harnesses, so it's using the **exact same logic that you would if you ran Claude Code**, Codex, or Amp natively.
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenCode (native): Model → OpenCode's tool loop → result
|
||||||
|
Gigacode: Model → Claude Code / Codex / Amp CLI → result
|
||||||
|
```
|
||||||
|
|
||||||
|
This means you get each agent's specialized capabilities (such as Claude Code's `Read`/`Write`/`Bash` tools, Codex's sandboxed execution, and Amp's permission rules) rather than a single tool loop with different models behind it.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
**macOS / Linux / WSL (Recommended)**
|
**macOS / Linux / WSL (Recommended)**
|
||||||
|
|
@ -73,8 +90,8 @@ gigacode
|
||||||
|
|
||||||
**Web UI**
|
**Web UI**
|
||||||
|
|
||||||
Use the [OpenCode Web UI](https://sandboxagent.dev/opencode-compatibility) to control any coding agent from the browser.
|
Use the [OpenCode Web UI](https://sandboxagent.dev/docs/opencode-compatibility) to control any coding agent from the browser.
|
||||||
|
|
||||||
**OpenCode SDK**
|
**OpenCode SDK**
|
||||||
|
|
||||||
Use the [`@opencode-ai/sdk`](https://sandboxagent.dev/opencode-compatibility) to programmatically control any coding agent.
|
Use the [`@opencode-ai/sdk`](https://sandboxagent.dev/docs/opencode-compatibility) to programmatically control any coding agent.
|
||||||
6
justfile
6
justfile
|
|
@ -66,15 +66,15 @@ install:
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build --filter @sandbox-agent/inspector...
|
pnpm build --filter @sandbox-agent/inspector...
|
||||||
cargo install --path server/packages/sandbox-agent --debug
|
cargo install --path server/packages/sandbox-agent --debug
|
||||||
cargo install --path server/packages/gigacode --debug
|
cargo install --path gigacode --debug
|
||||||
|
|
||||||
install-fast:
|
install-fast:
|
||||||
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo install --path server/packages/sandbox-agent --debug
|
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo install --path server/packages/sandbox-agent --debug
|
||||||
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo install --path server/packages/gigacode --debug
|
SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo install --path gigacode --debug
|
||||||
|
|
||||||
install-release:
|
install-release:
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build --filter @sandbox-agent/inspector...
|
pnpm build --filter @sandbox-agent/inspector...
|
||||||
cargo install --path server/packages/sandbox-agent
|
cargo install --path server/packages/sandbox-agent
|
||||||
cargo install --path server/packages/gigacode
|
cargo install --path gigacode
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -580,7 +580,7 @@ fn run_api(command: &ApiCommand, cli: &CliConfig) -> Result<(), CliError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_opencode(cli: &CliConfig, args: &OpencodeArgs) -> 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"))?;
|
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();
|
let token = cli.token.clone();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! stubbed responses with deterministic helpers for snapshot testing. A minimal
|
//! stubbed responses with deterministic helpers for snapshot testing. A minimal
|
||||||
//! in-memory state tracks sessions/messages/ptys to keep behavior coherent.
|
//! 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::convert::Infallible;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
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 PART_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||||
static PTY_COUNTER: AtomicU64 = AtomicU64::new(1);
|
static PTY_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||||
static PROJECT_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_MODEL_ID: &str = "mock";
|
||||||
|
const OPENCODE_DEFAULT_PROVIDER_ID: &str = "mock";
|
||||||
const OPENCODE_DEFAULT_AGENT_MODE: &str = "build";
|
const OPENCODE_DEFAULT_AGENT_MODE: &str = "build";
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -220,14 +219,19 @@ struct OpenCodeSessionRuntime {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct OpenCodeModelEntry {
|
struct OpenCodeModelEntry {
|
||||||
agent: AgentId,
|
|
||||||
model: AgentModelInfo,
|
model: AgentModelInfo,
|
||||||
|
group_id: String,
|
||||||
|
group_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct OpenCodeModelCache {
|
struct OpenCodeModelCache {
|
||||||
entries: Vec<OpenCodeModelEntry>,
|
entries: Vec<OpenCodeModelEntry>,
|
||||||
model_lookup: HashMap<String, AgentId>,
|
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,
|
default_model: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,13 +394,17 @@ async fn ensure_backing_session(
|
||||||
state: &Arc<OpenCodeAppState>,
|
state: &Arc<OpenCodeAppState>,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
|
model: Option<String>,
|
||||||
|
variant: Option<String>,
|
||||||
) -> Result<(), SandboxError> {
|
) -> Result<(), SandboxError> {
|
||||||
|
let model = model.filter(|value| !value.trim().is_empty());
|
||||||
|
let variant = variant.filter(|value| !value.trim().is_empty());
|
||||||
let request = CreateSessionRequest {
|
let request = CreateSessionRequest {
|
||||||
agent: agent.to_string(),
|
agent: agent.to_string(),
|
||||||
agent_mode: None,
|
agent_mode: None,
|
||||||
permission_mode: None,
|
permission_mode: None,
|
||||||
model: None,
|
model: model.clone(),
|
||||||
variant: None,
|
variant: variant.clone(),
|
||||||
agent_version: None,
|
agent_version: None,
|
||||||
};
|
};
|
||||||
match state
|
match state
|
||||||
|
|
@ -406,7 +414,17 @@ async fn ensure_backing_session(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => Ok(()),
|
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),
|
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 {
|
async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let mut model_lookup = HashMap::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;
|
let mut default_model: Option<String> = None;
|
||||||
|
|
||||||
for agent in available_agent_ids() {
|
for agent in available_agent_ids() {
|
||||||
|
|
@ -631,32 +653,97 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if default_model.is_none() {
|
let first_model_id = response.models.first().map(|model| model.id.clone());
|
||||||
default_model = response
|
|
||||||
.default_model
|
|
||||||
.clone()
|
|
||||||
.or_else(|| response.models.first().map(|model| model.id.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
for model in response.models {
|
for model in response.models {
|
||||||
let model_id = model.id.clone();
|
let model_id = model.id.clone();
|
||||||
if model_lookup.contains_key(&model_id) {
|
let (group_id, group_name) = group_for_agent_model(agent, &model_id);
|
||||||
continue;
|
|
||||||
|
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) {
|
let mut groups: BTreeMap<String, Vec<&OpenCodeModelEntry>> = BTreeMap::new();
|
||||||
OPENCODE_DEFAULT_MODEL_ID.to_string()
|
for entry in &entries {
|
||||||
} else {
|
groups
|
||||||
default_model.unwrap_or_else(|| OPENCODE_DEFAULT_MODEL_ID.to_string())
|
.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 {
|
OpenCodeModelCache {
|
||||||
entries,
|
entries,
|
||||||
model_lookup,
|
model_lookup,
|
||||||
|
group_defaults,
|
||||||
|
group_agents,
|
||||||
|
group_names,
|
||||||
|
default_group,
|
||||||
default_model,
|
default_model,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -666,8 +753,8 @@ fn resolve_agent_from_model(
|
||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Option<AgentId> {
|
) -> Option<AgentId> {
|
||||||
if provider_id != OPENCODE_PROVIDER_ID {
|
if let Some(agent) = cache.group_agents.get(provider_id) {
|
||||||
return None;
|
return Some(*agent);
|
||||||
}
|
}
|
||||||
if let Some(agent) = cache.model_lookup.get(model_id) {
|
if let Some(agent) = cache.model_lookup.get(model_id) {
|
||||||
return Some(*agent);
|
return Some(*agent);
|
||||||
|
|
@ -675,6 +762,9 @@ fn resolve_agent_from_model(
|
||||||
if let Some(agent) = AgentId::parse(model_id) {
|
if let Some(agent) = AgentId::parse(model_id) {
|
||||||
return Some(agent);
|
return Some(agent);
|
||||||
}
|
}
|
||||||
|
if opencode_group_provider(provider_id).is_some() {
|
||||||
|
return Some(AgentId::Opencode);
|
||||||
|
}
|
||||||
if model_id.contains('/') {
|
if model_id.contains('/') {
|
||||||
return Some(AgentId::Opencode);
|
return Some(AgentId::Opencode);
|
||||||
}
|
}
|
||||||
|
|
@ -706,15 +796,31 @@ async fn resolve_session_agent(
|
||||||
let default_model_id = cache.default_model.clone();
|
let default_model_id = cache.default_model.clone();
|
||||||
let mut provider_id = requested_provider
|
let mut provider_id = requested_provider
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.unwrap_or(OPENCODE_PROVIDER_ID)
|
.filter(|value| *value != "sandbox-agent")
|
||||||
.to_string();
|
.map(|value| value.to_string());
|
||||||
let mut model_id = requested_model
|
let model_id = requested_model
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.unwrap_or(default_model_id.as_str())
|
.map(|value| value.to_string());
|
||||||
.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);
|
let mut resolved_agent = resolve_agent_from_model(&cache, &provider_id, &model_id);
|
||||||
if resolved_agent.is_none() {
|
if resolved_agent.is_none() {
|
||||||
provider_id = OPENCODE_PROVIDER_ID.to_string();
|
provider_id = cache.default_group.clone();
|
||||||
model_id = default_model_id.clone();
|
model_id = default_model_id.clone();
|
||||||
resolved_agent =
|
resolved_agent =
|
||||||
resolve_agent_from_model(&cache, &provider_id, &model_id).or_else(|| Some(default_agent_id()));
|
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 {
|
fn opencode_model_provider(model_id: &str) -> Option<&str> {
|
||||||
let model_name = model.name.clone().unwrap_or_else(|| model.id.clone());
|
model_id.split_once('/').map(|(provider, _)| provider)
|
||||||
let variants = model_variants_object(model);
|
}
|
||||||
|
|
||||||
|
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!({
|
json!({
|
||||||
"id": model.id,
|
"id": entry.model.id,
|
||||||
"providerID": OPENCODE_PROVIDER_ID,
|
"providerID": entry.group_id,
|
||||||
"api": {
|
"api": {
|
||||||
"id": "sandbox-agent",
|
"id": "sandbox-agent",
|
||||||
"url": "http://localhost",
|
"url": "http://localhost",
|
||||||
"npm": "@sandbox-agent/sdk"
|
"npm": "@sandbox-agent/sdk"
|
||||||
},
|
},
|
||||||
"name": model_name,
|
"name": model_name,
|
||||||
"family": agent_display_name(agent),
|
"family": entry.group_name,
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"temperature": true,
|
"temperature": true,
|
||||||
"reasoning": 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 {
|
fn model_summary_entry(entry: &OpenCodeModelEntry) -> Value {
|
||||||
let model_name = model.name.clone().unwrap_or_else(|| model.id.clone());
|
let model_name = entry
|
||||||
let variants = model_variants_object(model);
|
.model
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| entry.model.id.clone());
|
||||||
|
let variants = model_variants_object(&entry.model);
|
||||||
json!({
|
json!({
|
||||||
"id": model.id,
|
"id": entry.model.id,
|
||||||
"name": model_name,
|
"name": model_name,
|
||||||
"family": agent_display_name(agent),
|
"family": entry.group_name,
|
||||||
"release_date": "2024-01-01",
|
"release_date": "2024-01-01",
|
||||||
"attachment": false,
|
"attachment": false,
|
||||||
"reasoning": true,
|
"reasoning": true,
|
||||||
|
|
@ -1673,7 +1827,7 @@ async fn apply_item_event(
|
||||||
let provider_id = runtime
|
let provider_id = runtime
|
||||||
.last_model_provider
|
.last_model_provider
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
|
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
|
||||||
let model_id = runtime
|
let model_id = runtime
|
||||||
.last_model_id
|
.last_model_id
|
||||||
.clone()
|
.clone()
|
||||||
|
|
@ -1984,7 +2138,7 @@ async fn apply_tool_item_event(
|
||||||
let provider_id = runtime
|
let provider_id = runtime
|
||||||
.last_model_provider
|
.last_model_provider
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
|
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
|
||||||
let model_id = runtime
|
let model_id = runtime
|
||||||
.last_model_id
|
.last_model_id
|
||||||
.clone()
|
.clone()
|
||||||
|
|
@ -2218,7 +2372,7 @@ async fn apply_item_delta(
|
||||||
let provider_id = runtime
|
let provider_id = runtime
|
||||||
.last_model_provider
|
.last_model_provider
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| OPENCODE_PROVIDER_ID.to_string());
|
.unwrap_or_else(|| OPENCODE_DEFAULT_PROVIDER_ID.to_string());
|
||||||
let model_id = runtime
|
let model_id = runtime
|
||||||
.last_model_id
|
.last_model_id
|
||||||
.clone()
|
.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 {
|
async fn oc_agent_list(State(state): State<Arc<OpenCodeAppState>>) -> impl IntoResponse {
|
||||||
let agent = json!({
|
let agent = json!({
|
||||||
"name": OPENCODE_PROVIDER_NAME,
|
"name": "Sandbox Agent",
|
||||||
"description": "Sandbox Agent compatibility layer",
|
"description": "Sandbox Agent compatibility layer",
|
||||||
"mode": "all",
|
"mode": "all",
|
||||||
"native": false,
|
"native": false,
|
||||||
|
|
@ -2472,28 +2626,41 @@ async fn oc_config_providers(
|
||||||
State(state): State<Arc<OpenCodeAppState>>,
|
State(state): State<Arc<OpenCodeAppState>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let cache = opencode_model_cache(&state).await;
|
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 {
|
for entry in &cache.entries {
|
||||||
models.insert(
|
grouped
|
||||||
entry.model.id.clone(),
|
.entry(entry.group_id.clone())
|
||||||
model_config_entry(entry.agent, &entry.model),
|
.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!({
|
let providers = json!({
|
||||||
"providers": [
|
"providers": providers,
|
||||||
{
|
"default": Value::Object(defaults),
|
||||||
"id": OPENCODE_PROVIDER_ID,
|
|
||||||
"name": OPENCODE_PROVIDER_NAME,
|
|
||||||
"source": "custom",
|
|
||||||
"env": [],
|
|
||||||
"key": "",
|
|
||||||
"options": {},
|
|
||||||
"models": Value::Object(models),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default": {
|
|
||||||
OPENCODE_PROVIDER_ID: cache.default_model
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
(StatusCode::OK, Json(providers))
|
(StatusCode::OK, Json(providers))
|
||||||
}
|
}
|
||||||
|
|
@ -3144,6 +3311,9 @@ async fn oc_session_message_create(
|
||||||
.and_then(|v| v.as_str());
|
.and_then(|v| v.as_str());
|
||||||
let (session_agent, provider_id, model_id) =
|
let (session_agent, provider_id, model_id) =
|
||||||
resolve_session_agent(&state, &session_id, requested_provider, requested_model).await;
|
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();
|
let parts_input = body.parts.unwrap_or_default();
|
||||||
if parts_input.is_empty() {
|
if parts_input.is_empty() {
|
||||||
|
|
@ -3207,7 +3377,15 @@ async fn oc_session_message_create(
|
||||||
})
|
})
|
||||||
.await;
|
.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!(
|
tracing::warn!(
|
||||||
target = "sandbox_agent::opencode",
|
target = "sandbox_agent::opencode",
|
||||||
?err,
|
?err,
|
||||||
|
|
@ -3413,7 +3591,7 @@ async fn oc_session_command(
|
||||||
&directory,
|
&directory,
|
||||||
&worktree,
|
&worktree,
|
||||||
&agent,
|
&agent,
|
||||||
OPENCODE_PROVIDER_ID,
|
OPENCODE_DEFAULT_PROVIDER_ID,
|
||||||
OPENCODE_DEFAULT_MODEL_ID,
|
OPENCODE_DEFAULT_MODEL_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -3463,7 +3641,7 @@ async fn oc_session_shell(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|v| v.get("providerID"))
|
.and_then(|v| v.get("providerID"))
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or(OPENCODE_PROVIDER_ID),
|
.unwrap_or(OPENCODE_DEFAULT_PROVIDER_ID),
|
||||||
body.model
|
body.model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|v| v.get("modelID"))
|
.and_then(|v| v.get("modelID"))
|
||||||
|
|
@ -3775,26 +3953,41 @@ async fn oc_provider_list(
|
||||||
State(state): State<Arc<OpenCodeAppState>>,
|
State(state): State<Arc<OpenCodeAppState>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let cache = opencode_model_cache(&state).await;
|
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 {
|
for entry in &cache.entries {
|
||||||
models.insert(
|
grouped
|
||||||
entry.model.id.clone(),
|
.entry(entry.group_id.clone())
|
||||||
model_summary_entry(entry.agent, &entry.model),
|
.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!({
|
let providers = json!({
|
||||||
"all": [
|
"all": providers,
|
||||||
{
|
"default": Value::Object(defaults),
|
||||||
"id": OPENCODE_PROVIDER_ID,
|
"connected": connected
|
||||||
"name": OPENCODE_PROVIDER_NAME,
|
|
||||||
"env": [],
|
|
||||||
"models": Value::Object(models),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default": {
|
|
||||||
OPENCODE_PROVIDER_ID: cache.default_model
|
|
||||||
},
|
|
||||||
"connected": [OPENCODE_PROVIDER_ID]
|
|
||||||
});
|
});
|
||||||
(StatusCode::OK, Json(providers))
|
(StatusCode::OK, Json(providers))
|
||||||
}
|
}
|
||||||
|
|
@ -3805,11 +3998,15 @@ async fn oc_provider_list(
|
||||||
responses((status = 200)),
|
responses((status = 200)),
|
||||||
tag = "opencode"
|
tag = "opencode"
|
||||||
)]
|
)]
|
||||||
async fn oc_provider_auth() -> impl IntoResponse {
|
async fn oc_provider_auth(
|
||||||
let auth = json!({
|
State(state): State<Arc<OpenCodeAppState>>,
|
||||||
OPENCODE_PROVIDER_ID: []
|
) -> impl IntoResponse {
|
||||||
});
|
let cache = opencode_model_cache(&state).await;
|
||||||
(StatusCode::OK, Json(auth))
|
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(
|
#[utoipa::path(
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
async fn agent_modes(&self, agent: AgentId) -> Result<Vec<AgentModeInfo>, SandboxError> {
|
||||||
if agent != AgentId::Opencode {
|
if agent != AgentId::Opencode {
|
||||||
return Ok(agent_modes_for(agent));
|
return Ok(agent_modes_for(agent));
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ describe("OpenCode-compatible Event Streaming", () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Say hello" }],
|
parts: [{ type: "text", text: "Say hello" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -180,7 +180,7 @@ describe("OpenCode-compatible Event Streaming", () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "echo hello" }],
|
parts: [{ type: "text", text: "echo hello" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -223,7 +223,7 @@ describe("OpenCode-compatible Event Streaming", () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "tool" }],
|
parts: [{ type: "text", text: "tool" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ describe("OpenCode-compatible Messaging API", () => {
|
||||||
const response = await client.session.prompt({
|
const response = await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Hello, world!" }],
|
parts: [{ type: "text", text: "Hello, world!" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -58,7 +58,7 @@ describe("OpenCode-compatible Messaging API", () => {
|
||||||
const response = await client.session.prompt({
|
const response = await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Say hello" }],
|
parts: [{ type: "text", text: "Say hello" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -72,7 +72,7 @@ describe("OpenCode-compatible Messaging API", () => {
|
||||||
const response = await client.session.promptAsync({
|
const response = await client.session.promptAsync({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Process this asynchronously" }],
|
parts: [{ type: "text", text: "Process this asynchronously" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -96,7 +96,7 @@ describe("OpenCode-compatible Messaging API", () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Test message" }],
|
parts: [{ type: "text", text: "Test message" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -116,7 +116,7 @@ describe("OpenCode-compatible Messaging API", () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Test" }],
|
parts: [{ type: "text", text: "Test" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -144,7 +144,7 @@ describe("OpenCode-compatible Messaging API", () => {
|
||||||
await client.session.promptAsync({
|
await client.session.promptAsync({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "Long running task" }],
|
parts: [{ type: "text", text: "Long running task" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,22 +28,25 @@ describe("OpenCode-compatible Model API", () => {
|
||||||
|
|
||||||
it("should list models grouped by agent with real model IDs", async () => {
|
it("should list models grouped by agent with real model IDs", async () => {
|
||||||
const response = await client.provider.list();
|
const response = await client.provider.list();
|
||||||
const provider = response.data?.all?.find((entry) => entry.id === "sandbox-agent");
|
const providers = response.data?.all ?? [];
|
||||||
expect(provider).toBeDefined();
|
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 mockModels = mockProvider?.models ?? {};
|
||||||
const modelIds = Object.keys(models);
|
expect(mockModels["mock"]).toBeDefined();
|
||||||
expect(modelIds.length).toBeGreaterThan(0);
|
expect(mockModels["mock"].id).toBe("mock");
|
||||||
|
expect(mockModels["mock"].family).toBe("Mock");
|
||||||
|
|
||||||
expect(models["mock"]).toBeDefined();
|
const ampModels = ampProvider?.models ?? {};
|
||||||
expect(models["mock"].id).toBe("mock");
|
expect(ampModels["smart"]).toBeDefined();
|
||||||
expect(models["mock"].family).toBe("Mock");
|
expect(ampModels["smart"].id).toBe("smart");
|
||||||
|
expect(ampModels["smart"].family).toBe("Amp");
|
||||||
|
|
||||||
expect(models["smart"]).toBeDefined();
|
expect(response.data?.default?.["mock"]).toBe("mock");
|
||||||
expect(models["smart"].id).toBe("smart");
|
expect(response.data?.default?.["amp"]).toBe("smart");
|
||||||
expect(models["smart"].family).toBe("Amp");
|
|
||||||
|
|
||||||
expect(models["amp"]).toBeUndefined();
|
|
||||||
expect(response.data?.default?.["sandbox-agent"]).toBe("mock");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ describe("OpenCode-compatible Permission API", () => {
|
||||||
it("should receive permission.asked and reply via global endpoint", async () => {
|
it("should receive permission.asked and reply via global endpoint", async () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
sessionID: sessionId,
|
sessionID: sessionId,
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: permissionPrompt }],
|
parts: [{ type: "text", text: permissionPrompt }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ describe("OpenCode-compatible Permission API", () => {
|
||||||
it("should accept permission response for a session", async () => {
|
it("should accept permission response for a session", async () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
sessionID: sessionId,
|
sessionID: sessionId,
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: permissionPrompt }],
|
parts: [{ type: "text", text: permissionPrompt }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ describe("OpenCode-compatible Question API", () => {
|
||||||
it("should ask a question and accept a reply", async () => {
|
it("should ask a question and accept a reply", async () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
sessionID: sessionId,
|
sessionID: sessionId,
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: questionPrompt }],
|
parts: [{ type: "text", text: questionPrompt }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ describe("OpenCode-compatible Question API", () => {
|
||||||
it("should allow rejecting a question", async () => {
|
it("should allow rejecting a question", async () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
sessionID: sessionId,
|
sessionID: sessionId,
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: questionPrompt }],
|
parts: [{ type: "text", text: questionPrompt }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ describe("OpenCode-compatible Tool + File Actions", () => {
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
model: { providerID: "sandbox-agent", modelID: "mock" },
|
model: { providerID: "mock", modelID: "mock" },
|
||||||
parts: [{ type: "text", text: "tool" }],
|
parts: [{ type: "text", text: "tool" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue