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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ repository = "https://github.com/edgee-ai/edgee"
[workspace.dependencies]
# External dependencies
anyhow = "1.0.102"
argon2 = "0.5.3"
base64 = "0.22"
bytes = "1.11.1"
clap = "4.6.1"
colored = "3.1.1"
Expand All @@ -20,6 +22,7 @@ http = "1.4.0"
http-body-util = "0.1.3"
hudsucker = "0.24"
open = "5.3.4"
rand = "0.10"
rcgen = "0.14"
reqwest = { version = "0.13.2", default-features = false }
regex = "1.12.3"
Expand All @@ -33,3 +36,4 @@ tracing = "0.1.44"
tracing-subscriber = "0.3"
unicode-width = "0.2"
uuid = "1.23.1"
x25519-dalek = "3.0.0"
4 changes: 4 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ path = "src/main.rs"
[dependencies]
# External dependencies
anyhow.workspace = true
argon2.workspace = true
base64.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
colored.workspace = true
console.workspace = true
dialoguer.workspace = true
etcetera.workspace = true
http.workspace = true
open.workspace = true
rand.workspace = true
serde = { workspace = true, features = ["derive"] }
toml.workspace = true
tracing.workspace = true
Expand All @@ -28,6 +31,7 @@ serde_json.workspace = true
self_update = { workspace = true, optional = true, features = ["reqwest", "rustls"] }
time = { workspace = true, features = ["formatting", "parsing", "serde", "serde-human-readable", "macros"] }
unicode-width.workspace = true
x25519-dalek = { workspace = true, features = ["static_secrets", "getrandom"] }

# Local MITM relay (`edgee relay` / app launch targets)
bytes.workspace = true
Expand Down
10 changes: 9 additions & 1 deletion crates/cli/src/commands/launch/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,19 @@ pub async fn run(opts: Options) -> Result<()> {
util::spawn_cli_version_report(&creds, &session_id);

let gateway_url = super::resolve_gateway_base_url(&creds).await;
let debug_log_header = util::resolve_debug_log_keypair()?
.map(|keypair| {
let headers = keypair.header_values();
format!("\nx-edgee-debug-pubkey: {}\nx-edgee-debug-salt: {}", headers.pubkey, headers.salt)
})
.unwrap_or_default();
let mut cmd = std::process::Command::new(util::resolve_binary("claude"));
cmd.env("ANTHROPIC_BASE_URL", &gateway_url);
cmd.env(
"ANTHROPIC_CUSTOM_HEADERS",
format!("x-edgee-api-key: {api_key}\nx-edgee-session-id: {session_id}{repo_header}"),
format!(
"x-edgee-api-key: {api_key}\nx-edgee-session-id: {session_id}{repo_header}{debug_log_header}"
),
);
cmd.env("EDGEE_SESSION_ID", &session_id);
cmd.env(
Expand Down
8 changes: 7 additions & 1 deletion crates/cli/src/commands/launch/codebuddy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ pub async fn run(opts: Options) -> Result<()> {
let repo_entry = crate::git::detect_origin()
.map(|url| format!(",\"x-edgee-repo\"=\"{url}\""))
.unwrap_or_default();
let debug_log_header = util::resolve_debug_log_keypair()?
.map(|keypair| {
let headers = keypair.header_values();
format!("\nx-edgee-debug-pubkey: {}\nx-edgee-debug-salt: {}", headers.pubkey, headers.salt)
})
.unwrap_or_default();
let base_url = format!("{}/v1", super::resolve_gateway_base_url(&creds).await);
let mut cmd = std::process::Command::new(util::resolve_binary("codebuddy"));
cmd.env("EDGEE_SESSION_ID", &session_id);
cmd.env("CODEBUDDY_BASE_URL", &base_url);
cmd.env(
"CODEBUDDY_CUSTOM_HEADERS",
format!(
"x-edgee-api-key: {api_key}\nx-edgee-session-id: {session_id}{repo_entry}"
"x-edgee-api-key: {api_key}\nx-edgee-session-id: {session_id}{repo_entry}{debug_log_header}"
),
);
cmd.args(&opts.args);
Expand Down
8 changes: 7 additions & 1 deletion crates/cli/src/commands/launch/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ pub async fn run(opts: Options) -> Result<()> {
let repo_entry = crate::git::detect_origin()
.map(|url| format!(",\"x-edgee-repo\"=\"{url}\""))
.unwrap_or_default();
let debug_log_entry = util::resolve_debug_log_keypair()?
.map(|keypair| {
let headers = keypair.header_values();
format!(",\"x-edgee-debug-pubkey\"=\"{}\",\"x-edgee-debug-salt\"=\"{}\"", headers.pubkey, headers.salt)
})
.unwrap_or_default();
let base_url = format!("{}/v1", super::resolve_gateway_base_url(&creds).await);
let mut cmd = std::process::Command::new(util::resolve_binary("codex"));
cmd.env("EDGEE_SESSION_ID", &session_id);
cmd.args([
"-c", "model_provider=\"edgee-cli\"",
"-c", "model_providers.edgee-cli.name=\"EDGEE\"",
"-c", &format!("model_providers.edgee-cli.base_url=\"{base_url}\""),
"-c", &format!("model_providers.edgee-cli.http_headers={{\"x-edgee-api-key\"=\"{api_key}\",\"x-edgee-session-id\"=\"{session_id}\"{repo_entry}}}"),
"-c", &format!("model_providers.edgee-cli.http_headers={{\"x-edgee-api-key\"=\"{api_key}\",\"x-edgee-session-id\"=\"{session_id}\"{repo_entry}{debug_log_entry}}}"),
"-c", "model_providers.edgee-cli.wire_api=\"responses\"",
]);
cmd.args(&opts.args);
Expand Down
21 changes: 16 additions & 5 deletions crates/cli/src/commands/launch/crush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,31 @@ fn build_edgee_provider(
session_id: &str,
gateway_url: &str,
models: &[String],
debug_log_headers: Option<crate::crypto::DebugLogHeaderValues>,
) -> Value {
// The provider is an OpenAI-compatible endpoint pointed at the gateway's
// `/v1`. The gateway `id` (e.g. `anthropic/claude-opus-4-8`) is already the
// routing identifier the gateway accepts, so it serves as both the model id
// and the display name. When the listing is empty we leave `models` off and
// set `discover_models` so Crush populates the picker from `/v1/models`.
let mut extra_headers = serde_json::json!({
"x-edgee-api-key": api_key,
"x-edgee-session-id": session_id,
});
if let (Some(headers_obj), Some(debug_headers)) =
(extra_headers.as_object_mut(), debug_log_headers)
{
headers_obj.insert("x-edgee-debug-pubkey".to_string(), Value::String(debug_headers.pubkey));
headers_obj.insert("x-edgee-debug-salt".to_string(), Value::String(debug_headers.salt));
}

let mut provider = serde_json::json!({
"id": "edgee",
"name": "Edgee",
"type": "openai-compat",
"base_url": format!("{}/v1", gateway_url),
"api_key": api_key,
"extra_headers": {
"x-edgee-api-key": api_key,
"x-edgee-session-id": session_id,
},
"extra_headers": extra_headers,
"discover_models": true,
});

Expand Down Expand Up @@ -198,7 +207,9 @@ pub async fn run(opts: Options) -> Result<()> {
});

let models = fetch_gateway_models(&gateway_url, api_key).await;
let edgee_provider = build_edgee_provider(api_key, &session_id, &gateway_url, &models);
let debug_log_headers = util::resolve_debug_log_keypair()?.map(|k| k.header_values());
let edgee_provider =
build_edgee_provider(api_key, &session_id, &gateway_url, &models, debug_log_headers);
insert_edgee_provider(&mut config, edgee_provider);

// Crush reads `crush.json` from the directory named by CRUSH_GLOBAL_CONFIG,
Expand Down
19 changes: 14 additions & 5 deletions crates/cli/src/commands/launch/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,22 +171,29 @@ fn build_edgee_provider(
session_id: &str,
gateway_url: &str,
models: &[String],
debug_log_headers: Option<crate::crypto::DebugLogHeaderValues>,
) -> Value {
// The provider is `@ai-sdk/openai-compatible` pointed at the gateway's
// `/v1`. We fill the `models` map from the gateway's `/v1/models` listing so
// OpenCode's picker is populated with the live catalog. The gateway `id`
// (e.g. `anthropic/claude-opus-4-8`) is already the routing identifier the
// gateway accepts, so it serves as both the map key and the display name.
let mut headers = serde_json::json!({
"x-edgee-api-key": api_key,
"x-edgee-session-id": session_id,
});
if let (Some(headers_obj), Some(debug_headers)) = (headers.as_object_mut(), debug_log_headers) {
headers_obj.insert("x-edgee-debug-pubkey".to_string(), Value::String(debug_headers.pubkey));
headers_obj.insert("x-edgee-debug-salt".to_string(), Value::String(debug_headers.salt));
}

let mut provider = serde_json::json!({
"npm": "@ai-sdk/openai-compatible",
"name": "Edgee",
"options": {
"baseURL": format!("{}/v1", gateway_url),
"apiKey": api_key,
"headers": {
"x-edgee-api-key": api_key,
"x-edgee-session-id": session_id,
}
"headers": headers,
}
});

Expand Down Expand Up @@ -253,7 +260,9 @@ pub async fn run(opts: Options) -> Result<()> {
});

let models = fetch_gateway_models(&gateway_url, api_key).await;
let edgee_provider = build_edgee_provider(api_key, &session_id, &gateway_url, &models);
let debug_log_headers = util::resolve_debug_log_keypair()?.map(|k| k.header_values());
let edgee_provider =
build_edgee_provider(api_key, &session_id, &gateway_url, &models, debug_log_headers);

if let Some(obj) = config.as_object_mut() {
if let Some(providers) = obj.get_mut("provider") {
Expand Down
Loading