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
20 changes: 20 additions & 0 deletions crates/cli/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ pub struct Organization {
pub id: String,
pub slug: String,
pub name: String,
/// The gateway base URL configured for this org in the console (region or
/// self-hosted). Absent/empty when never set; the launch path then falls
/// back to a local override or the built-in default.
#[serde(default, rename = "gateway_api_url")]
pub gateway_url: Option<String>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -220,6 +225,21 @@ impl ApiClient {
Ok(body.data)
}

/// Fetches a single organization (`GET /v1/organizations/{org}`). Used at
/// launch to read the org's configured `gateway_api_url` fresh, so a console
/// change takes effect on the next launch without re-login.
pub async fn get_organization(&self, org_id: &str) -> Result<Organization> {
let url = format!("{}/v1/organizations/{}", self.base_url, org_id);
let resp = self
.http
.get(&url)
.send()
.await
.context("Failed to fetch organization")?;
check_status(&resp, "fetch organization")?;
resp.json().await.context("Invalid organization response")
}

/// Lists the gateway model catalog (with `plan_fallback`, `aliases`, etc.) used
/// to offer fallback/reroute targets. Served by the console API
/// (`console_api_base_url`, e.g. `api.edgee.app`) — not the gateway, whose
Expand Down
3 changes: 2 additions & 1 deletion crates/cli/src/commands/launch/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ 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 mut cmd = std::process::Command::new(util::resolve_binary("claude"));
cmd.env("ANTHROPIC_BASE_URL", crate::config::gateway_base_url());
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}"),
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/launch/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ 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 base_url = format!("{}/v1", crate::config::gateway_base_url());
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([
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/launch/crush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ pub async fn run(opts: Options) -> Result<()> {
// exactly once (Claude Code-targeted; honors the disable marker).
util::ensure_first_run_installed().await;

let gateway_url = crate::config::gateway_base_url();
let gateway_url = super::resolve_gateway_base_url(&creds).await;

let mut config = find_global_config().unwrap_or_else(|| {
serde_json::json!({
Expand Down
39 changes: 39 additions & 0 deletions crates/cli/src/commands/launch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,45 @@ async fn print_session_stats(
}
}

/// Resolves the gateway base URL for a launch.
///
/// Precedence (highest first):
/// 1. `EDGEE_API_URL` env var — the explicit, ephemeral escape hatch (local
/// debugging, incident response).
/// 2. The active profile's persisted `gateway_url` — the user's local choice.
/// 3. The org's console-configured `gateway_api_url` — server default when the
/// user hasn't set anything locally.
/// 4. The built-in default.
///
/// Local overrides win over the server value; the server only fills in when the
/// user has no local preference. The org fetch is best-effort: any failure
/// falls through to the next source so launch never breaks (offline, no org
/// selected, or no configured gateway).
pub async fn resolve_gateway_base_url(creds: &crate::config::Credentials) -> String {
if let Some(env_url) = crate::config::gateway_url_env_override() {
return env_url;
}

if let Some(profile_url) = crate::config::gateway_url_profile_override() {
return profile_url;
}

if let (Some(token), Some(org_id)) = (
creds.user_token.as_deref().filter(|t| !t.is_empty()),
creds.org_id.as_deref().filter(|o| !o.is_empty()),
) {
if let Ok(client) = crate::api::ApiClient::new(token) {
if let Ok(org) = client.get_organization(org_id).await {
if let Some(url) = org.gateway_url.filter(|s| !s.is_empty()) {
return url;
}
}
}
}

crate::config::DEFAULT_GATEWAY_URL.to_string()
}

async fn fetch_stats(
creds: &crate::config::Credentials,
session_id: &str,
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/launch/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ pub async fn run(opts: Options) -> Result<()> {
// exactly once (Claude Code-targeted; honors the disable marker).
util::ensure_first_run_installed().await;

let gateway_url = crate::config::gateway_base_url();
let gateway_url = super::resolve_gateway_base_url(&creds).await;

let mut config = find_user_config().unwrap_or_else(|| {
serde_json::json!({
Expand Down
25 changes: 20 additions & 5 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,29 @@ pub fn console_api_base_url() -> String {
.unwrap_or_else(|| "https://api.edgee.app".to_string())
}

pub fn gateway_base_url() -> String {
if let Ok(v) = std::env::var("EDGEE_API_URL") {
return v;
}
/// Built-in fallback gateway used when neither a local override nor an
/// org-configured gateway URL is available.
pub const DEFAULT_GATEWAY_URL: &str = "https://api.edgee.ai";

/// The `EDGEE_API_URL` env-var gateway override, if set and non-empty. This is
/// the explicit escape hatch (e.g. pointing at `http://localhost:5000` for local
/// debugging) and outranks everything, including the org's console-configured
/// gateway; see `commands::launch::resolve_gateway_base_url`.
pub fn gateway_url_env_override() -> Option<String> {
std::env::var("EDGEE_API_URL")
.ok()
.filter(|s| !s.is_empty())
}

/// The active profile's persisted `gateway_url`, if set and non-empty. A local
/// user preference: the launch path layers it above the org's console-configured
/// gateway and below the `EDGEE_API_URL` env override; see
/// `commands::launch::resolve_gateway_base_url`.
pub fn gateway_url_profile_override() -> Option<String> {
read()
.ok()
.and_then(|p| p.gateway_url)
.unwrap_or_else(|| "https://api.edgee.ai".to_string())
.filter(|s| !s.is_empty())
}

pub fn mcp_base_url() -> String {
Expand Down