Skip to content
Open
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What this repo is

Edgee is an **open-source AI Gateway** written in Rust. It sits between coding agents (Claude Code, Codex, OpenCode — Cursor and OpenClaw coming soon) or any llm client and LLM providers (Anthropic, OpenAI) and compresses token-heavy traffic on the fly. A hosted / edge version of the same gateway is available at [`www.edgee.ai`](https://www.edgee.ai); **this repository is the OSS core** you can self-host.
Edgee is an **open-source AI Gateway** written in Rust. It sits between coding agents (Claude Code, CodeBuddy, Codex, OpenCode — Cursor and OpenClaw coming soon) or any llm client and LLM providers (Anthropic, OpenAI) and compresses token-heavy traffic on the fly. A hosted / edge version of the same gateway is available at [`www.edgee.ai`](https://www.edgee.ai); **this repository is the OSS core** you can self-host.

The distinguishing feature is the compression engine. Today it ships a single technique — **tool-results compression** — but the architecture is explicitly designed to host **multiple composable techniques** that a developer selects and combines per request. When extending compression, add a new technique alongside the existing ones rather than threading a new code path through the provider dispatch layer.

Expand All @@ -20,7 +20,7 @@ If `edgee stats` fails, you have the wrong package installed.

Entry point: `crates/cli/src/main.rs`. Subcommands declared in `crates/cli/src/commands/mod.rs`:

- `edgee launch {claude|codex|opencode}` — launches the agent with `ANTHROPIC_BASE_URL` and custom headers pointing at the local gateway. Implementation per agent under `crates/cli/src/commands/launch/`.
- `edgee launch {claude|codebuddy|codex|opencode}` — launches the agent with `ANTHROPIC_BASE_URL` and custom headers pointing at the local gateway. Implementation per agent under `crates/cli/src/commands/launch/`.
- `edgee auth {login|status|list|switch}` — OAuth-style flow against the Edgee console. See `crates/cli/src/api.rs` and `crates/cli/src/commands/auth/`.
- `edgee stats` (visible alias `report`) — prints session token counts and compression savings.
- `edgee alias` — installs shell aliases for quick access.
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ edgee launch codex

# Opencode
edgee launch opencode

# CodeBuddy
edgee launch codebuddy
```

Any extra flags you pass after the subcommand are forwarded straight to the underlying agent. For example, to resume the most recent session:
Expand All @@ -83,21 +86,22 @@ Any extra flags you pass after the subcommand are forwarded straight to the unde
edgee launch claude --resume abcd # continue the last Claude Code session
edgee launch codex resume # resume the last Codex session
edgee launch opencode -c # continue the last OpenCode session
edgee launch codebuddy --resume <id> # resume the last CodeBuddy session
```

### Route plain `claude` / `codex` / `opencode` through Edgee

If you'd rather type `claude` (or have another tool spawn `claude` for you), install Edgee's shims:

```bash
edgee alias # installs all three; pass `claude`, `codex`, `opencode` to scope
edgee alias # installs all three; pass `claude`, `codebuddy`, `codex`, `opencode` to scope
edgee alias remove # to undo
```

This does two things:

1. Adds a shell alias to `~/.bashrc`, `~/.zshrc`, and `~/.config/fish/config.fish` (`alias claude='edgee launch claude'`, etc.) so interactive shells route through Edgee.
2. Writes executable shim scripts to `~/.edgee/bin/{claude,codex,opencode}` and prepends `~/.edgee/bin` to `PATH` in the same rc block. This means **non-interactive** shells, including `bash -c '...'`, scripts, and tools that spawn Claude Code via `exec`, also get routed through Edgee. Reopen your terminal (or `exec $SHELL -l`) once after install.
2. Writes executable shim scripts to `~/.edgee/bin/{claude,codebuddy,codex,opencode}` and prepends `~/.edgee/bin` to `PATH` in the same rc block. This means **non-interactive** shells, including `bash -c '...'`, scripts, and tools that spawn Claude Code via `exec`, also get routed through Edgee. Reopen your terminal (or `exec $SHELL -l`) once after install.

### Use as a standalone gateway

Expand Down Expand Up @@ -192,6 +196,7 @@ The `SessionStart` hook installed by `edgee statusline claude install` (or by th
| Claude Code | `edgee launch claude` | ✅ Supported |
| Codex | `edgee launch codex` | ✅ Supported |
| Opencode | `edgee launch opencode` | ✅ Supported |
| CodeBuddy | `edgee launch codebuddy` | ✅ Supported (local gateway) |
| Cursor | `edgee launch cursor` | 🔜 Coming soon |
| Any OpenAI-compatible client | `edgee serve` | ✅ Supported |

Expand Down
10 changes: 8 additions & 2 deletions crates/cli/src/commands/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ const SHIM_DIR_REL: &str = ".edgee/bin";
const USES_SHIMS: bool = cfg!(unix);

const CLAUDE_ALIAS: AliasSpec = AliasSpec::new("claude", "edgee launch claude");
const CODEBUDDY_ALIAS: AliasSpec = AliasSpec::new("codebuddy", "edgee launch codebuddy");
const CODEX_ALIAS: AliasSpec = AliasSpec::new("codex", "edgee launch codex");
const OPENCODE_ALIAS: AliasSpec = AliasSpec::new("opencode", "edgee launch opencode");

const ALL_ALIASES: [AliasSpec; 3] = [CLAUDE_ALIAS, CODEX_ALIAS, OPENCODE_ALIAS];
const ALL_ALIASES: [AliasSpec; 4] = [CLAUDE_ALIAS, CODEBUDDY_ALIAS, CODEX_ALIAS, OPENCODE_ALIAS];

const PATH_EXPORT_POSIX: &str = "case \":$PATH:\" in\n *\":$HOME/.edgee/bin:\"*) ;;\n *) export PATH=\"$HOME/.edgee/bin:$PATH\" ;;\nesac\n";
const PATH_EXPORT_FISH: &str = "fish_add_path -p \"$HOME/.edgee/bin\"\n";

#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
pub enum Agent {
Claude,
Codebuddy,
Codex,
Opencode,
All,
Expand All @@ -33,6 +35,7 @@ impl Agent {
fn aliases(self) -> &'static [AliasSpec] {
match self {
Self::Claude => std::slice::from_ref(&CLAUDE_ALIAS),
Self::Codebuddy => std::slice::from_ref(&CODEBUDDY_ALIAS),
Self::Codex => std::slice::from_ref(&CODEX_ALIAS),
Self::Opencode => std::slice::from_ref(&OPENCODE_ALIAS),
Self::All => &ALL_ALIASES,
Expand All @@ -42,9 +45,10 @@ impl Agent {
fn label(self) -> &'static str {
match self {
Self::Claude => "claude",
Self::Codebuddy => "codebuddy",
Self::Codex => "codex",
Self::Opencode => "opencode",
Self::All => "claude, codex, and opencode",
Self::All => "claude, codebuddy, codex, and opencode",
}
}
}
Expand Down Expand Up @@ -646,11 +650,13 @@ mod tests {
let dir = tmp.path().join("bin");
write_shims(&dir, &ALL_ALIASES).unwrap();
assert!(dir.join("claude").exists());
assert!(dir.join("codebuddy").exists());
assert!(dir.join("codex").exists());
assert!(dir.join("opencode").exists());

remove_shims(&dir, &codex_only()).unwrap();
assert!(dir.join("claude").exists());
assert!(dir.join("codebuddy").exists());
assert!(!dir.join("codex").exists());
assert!(dir.join("opencode").exists());
}
Expand Down
9 changes: 9 additions & 0 deletions crates/cli/src/commands/auth/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ pub async fn ensure_onboarded(provider: &str) -> Result<()> {
pub fn agent_label(provider: &str) -> &'static str {
match provider {
"claude" => "Claude Code",
"codebuddy" => "CodeBuddy",
"codex" => "Codex",
"opencode" => "OpenCode",
_ => "your agent",
Expand Down Expand Up @@ -283,6 +284,7 @@ pub async fn fetch_provider_key(provider: &str) -> Result<crate::api::ApiKeyItem
fn coding_assistant_name(provider: &str) -> Result<&'static str> {
match provider {
"claude" => Ok("claude_code"),
"codebuddy" => Ok("codebuddy"),
"codex" => Ok("codex"),
"opencode" => Ok("opencode"),
_ => anyhow::bail!("Unsupported provider `{provider}`"),
Expand All @@ -295,6 +297,7 @@ fn provider_config_mut<'a>(
) -> Result<&'a mut Option<crate::config::ProviderConfig>> {
match provider {
"claude" => Ok(&mut creds.claude),
"codebuddy" => Ok(&mut creds.codebuddy),
"codex" => Ok(&mut creds.codex),
"opencode" => Ok(&mut creds.opencode),
_ => anyhow::bail!("Unsupported provider `{provider}`"),
Expand All @@ -307,6 +310,7 @@ fn provider_config<'a>(
) -> Result<Option<&'a crate::config::ProviderConfig>> {
match provider {
"claude" => Ok(creds.claude.as_ref()),
"codebuddy" => Ok(creds.codebuddy.as_ref()),
"codex" => Ok(creds.codex.as_ref()),
"opencode" => Ok(creds.opencode.as_ref()),
_ => anyhow::bail!("Unsupported provider `{provider}`"),
Expand Down Expand Up @@ -471,6 +475,7 @@ mod tests {
#[test]
fn maps_provider_to_coding_assistant_name() {
assert_eq!(coding_assistant_name("claude").unwrap(), "claude_code");
assert_eq!(coding_assistant_name("codebuddy").unwrap(), "codebuddy");
assert_eq!(coding_assistant_name("codex").unwrap(), "codex");
assert_eq!(coding_assistant_name("opencode").unwrap(), "opencode");
assert!(coding_assistant_name("unknown").is_err());
Expand All @@ -483,6 +488,9 @@ mod tests {
provider_config_mut(&mut creds, "claude")
.unwrap()
.replace(crate::config::ProviderConfig::default());
provider_config_mut(&mut creds, "codebuddy")
.unwrap()
.replace(crate::config::ProviderConfig::default());
provider_config_mut(&mut creds, "codex")
.unwrap()
.replace(crate::config::ProviderConfig::default());
Expand All @@ -491,6 +499,7 @@ mod tests {
.replace(crate::config::ProviderConfig::default());

assert!(creds.claude.is_some());
assert!(creds.codebuddy.is_some());
assert!(creds.codex.is_some());
assert!(creds.opencode.is_some());
}
Expand Down
47 changes: 47 additions & 0 deletions crates/cli/src/commands/launch/codebuddy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use anyhow::Result;

use super::util;

#[derive(Debug, clap::Parser)]
#[command(disable_help_flag = true)]
pub struct Options {
/// Extra args passed through to the codebuddy CLI
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
}

/// Launch CodeBuddy routed through a local Edgee gateway.
///
/// CodeBuddy only supports the local-gateway flow today: its traffic is
/// Anthropic Messages API, so the gateway's `/v1/messages` route handles it
/// directly. Hosted mode is not yet supported because there is no documented
/// way to inject Edgee's `x-edgee-api-key` / `x-edgee-session-id` headers
/// into CodeBuddy's outbound requests.
pub async fn run(opts: Options) -> Result<()> {
use std::net::Ipv4Addr;

// First-run: install the persistent user-level statusline integration
// exactly once. CodeBuddy itself doesn't render an Edgee statusline today,
// but users typically also use Claude Code in the same shell — running
// the installer on the first `edgee launch` of any agent matches the
// "set it up once" flow we want.
util::ensure_first_run_installed().await;

let log_path = crate::config::local_gateway_log_path();
crate::local_gateway::init_file_tracing(&log_path)?;
eprintln!("edgee: gateway logs -> {}", log_path.display());

let gateway = crate::local_gateway::start((Ipv4Addr::LOCALHOST, 0).into()).await?;
let addr = gateway.addr;

let mut cmd = tokio::process::Command::new(util::resolve_binary("codebuddy"));
cmd.env("CODEBUDDY_BASE_URL", format!("http://{addr}"));
cmd.args(&opts.args);

util::run_with_gateway(
gateway,
cmd,
"CodeBuddy is not installed. Install it from https://cnb.cool/codebuddy/codebuddy-code",
)
.await
}
5 changes: 5 additions & 0 deletions crates/cli/src/commands/launch/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod claude;
pub mod codebuddy;
pub mod codex;
pub mod opencode;
mod util;
Expand All @@ -17,6 +18,9 @@ enum Command {
/// Launch OpenCode routed through Edgee
#[command(name = "opencode")]
OpenCode(opencode::Options),
/// Launch CodeBuddy routed through Edgee
#[command(name = "codebuddy")]
CodeBuddy(codebuddy::Options),
}

#[derive(Debug, clap::Parser)]
Expand All @@ -28,6 +32,7 @@ pub struct Options {
pub async fn run(opts: Options) -> anyhow::Result<()> {
match opts.command {
Command::Claude(o) => claude::run(o).await,
Command::CodeBuddy(o) => codebuddy::run(o).await,
Command::Codex(o) => codex::run(o).await,
Command::OpenCode(o) => opencode::run(o).await,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::api::{ApiClient, Compression, GatewayModel, KeySettings, ModelRoute,
use crate::commands::auth::login;

/// Coding agents whose keys can be configured. Order is reused for the interactive picker.
const PROVIDERS: [&str; 3] = ["claude", "codex", "opencode"];
const PROVIDERS: [&str; 4] = ["claude", "codebuddy", "codex", "opencode"];

#[derive(Debug, clap::Parser)]
pub struct Options {
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct Profile {
/// When false, no MCP config or system prompt is injected into the coding assistant.
pub enable_mcp: Option<bool>,
pub claude: Option<ProviderConfig>,
pub codebuddy: Option<ProviderConfig>,
pub codex: Option<ProviderConfig>,
pub opencode: Option<ProviderConfig>,
}
Expand Down
2 changes: 1 addition & 1 deletion crates/compression-layer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Tower `Layer`/`Service` that compresses LLM tool outputs in-flight.
This crate wraps any downstream Tower service and intercepts requests before they leave the gateway. It calls `edgee-compressor` to shrink tool-result payloads, then forwards the mutated request to the inner service. Only tool results are modified; all other request fields pass through unchanged.

```
coding agent (Claude Code / Codex / OpenCode)
coding agent (Claude Code / CodeBuddy / Codex / OpenCode)
|
edgee-compression-layer <-- this crate
CompressionLayer
Expand Down
2 changes: 1 addition & 1 deletion crates/gateway-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Core LLM request/response pipeline for the Edgee AI Gateway.
This crate is the foundation that all other gateway crates build on. It defines the canonical request/response types (OpenAI Chat Completions format), the `Provider` trait, and the two working passthrough services for Anthropic and OpenAI. It has no hard dependency on tokio or reqwest, making it portable to any async runtime including `wasm32-wasip1`.

```
coding agent (Claude Code / Codex / OpenCode)
coding agent (Claude Code / CodeBuddy / Codex / OpenCode)
|
compression-layer (edgee-compression-layer)
|
Expand Down
2 changes: 1 addition & 1 deletion doc/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document describes the design of the Edgee AI Gateway OSS codebase: the Tow

## Overview

Edgee is an LLM gateway that sits between a coding agent (Claude Code, Codex, OpenCode) and an LLM provider (Anthropic, OpenAI). Its primary function today is **tool-output compression**: before each API request is forwarded, tool results in the context window are analyzed and shrunk to reduce token count without changing the model's view of the conversation.
Edgee is an LLM gateway that sits between a coding agent (Claude Code, CodeBuddy, Codex, OpenCode) and an LLM provider (Anthropic, OpenAI). Its primary function today is **tool-output compression**: before each API request is forwarded, tool results in the context window are analyzed and shrunk to reduce token count without changing the model's view of the conversation.

The gateway is built on **[Tower](https://docs.rs/tower/latest/tower/)**, a Rust middleware framework. Every processing step is a Tower [`Service`](https://docs.rs/tower/latest/tower/trait.Service.html), and processing steps are composed by stacking Tower [`Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html)s. This design means the compression pipeline, the HTTP boundary, and the provider dispatch are all independently testable units that can be composed in different configurations using [`ServiceBuilder`](https://docs.rs/tower/latest/tower/builder/struct.ServiceBuilder.html).

Expand Down