From 71a94f906304fa295fefaff0da4cbf2003fccb52 Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Thu, 25 Jun 2026 17:38:58 +0200 Subject: [PATCH 1/8] Split gateway URL resolution into local override + default --- crates/cli/src/config.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 332d13c..6d9a913 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -278,14 +278,24 @@ pub fn console_api_base_url() -> String { .unwrap_or_else(|| "https://api.edgee.app".to_string()) } -pub fn gateway_base_url() -> String { +/// 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"; + +/// An explicit *local* gateway override, if one is set: the `EDGEE_API_URL` env +/// var (highest priority) or the active profile's `gateway_url`. The launch path +/// layers the org's console-configured gateway below this and above the default; +/// see `commands::launch::resolve_gateway_base_url`. +pub fn gateway_url_local_override() -> Option { if let Ok(v) = std::env::var("EDGEE_API_URL") { - return v; + if !v.is_empty() { + return Some(v); + } } 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 { From e024716e04d216a02fcc09fb66c54ce86cea91dc Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Thu, 25 Jun 2026 17:38:59 +0200 Subject: [PATCH 2/8] Add get_organization and gateway_api_url to API client --- crates/cli/src/api.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/cli/src/api.rs b/crates/cli/src/api.rs index 642559d..ded44d8 100644 --- a/crates/cli/src/api.rs +++ b/crates/cli/src/api.rs @@ -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, } #[derive(Deserialize)] @@ -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 { + 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 From b3e6b6820b2dee021bde64609aed9db0176d3f32 Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Thu, 25 Jun 2026 17:39:00 +0200 Subject: [PATCH 3/8] Add launch-time gateway URL resolver with org override --- crates/cli/src/commands/launch/mod.rs | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/cli/src/commands/launch/mod.rs b/crates/cli/src/commands/launch/mod.rs index 106fd9a..40ba16a 100644 --- a/crates/cli/src/commands/launch/mod.rs +++ b/crates/cli/src/commands/launch/mod.rs @@ -88,6 +88,34 @@ async fn print_session_stats( } } +/// Resolves the gateway base URL for a launch. +/// +/// Precedence (highest first): an explicit local override (`EDGEE_API_URL` env +/// or the profile's `gateway_url`), then the org's console-configured +/// `gateway_api_url`, then the built-in default. The org fetch is best-effort: +/// any failure (offline, no org selected, transient error) falls through to the +/// next source so launch never breaks. +pub async fn resolve_gateway_base_url(creds: &crate::config::Credentials) -> String { + if let Some(local) = crate::config::gateway_url_local_override() { + return local; + } + + 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, From 3d03fdc046006a03eb5e530196b0141ec579af74 Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Thu, 25 Jun 2026 17:39:01 +0200 Subject: [PATCH 4/8] Use resolved gateway URL in all launch commands --- crates/cli/src/commands/launch/claude.rs | 3 ++- crates/cli/src/commands/launch/codex.rs | 2 +- crates/cli/src/commands/launch/crush.rs | 2 +- crates/cli/src/commands/launch/opencode.rs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/cli/src/commands/launch/claude.rs b/crates/cli/src/commands/launch/claude.rs index a3c97ba..d55b66c 100644 --- a/crates/cli/src/commands/launch/claude.rs +++ b/crates/cli/src/commands/launch/claude.rs @@ -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}"), diff --git a/crates/cli/src/commands/launch/codex.rs b/crates/cli/src/commands/launch/codex.rs index 0cfd38c..3eba715 100644 --- a/crates/cli/src/commands/launch/codex.rs +++ b/crates/cli/src/commands/launch/codex.rs @@ -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([ diff --git a/crates/cli/src/commands/launch/crush.rs b/crates/cli/src/commands/launch/crush.rs index 14d61d5..2f7b0c9 100644 --- a/crates/cli/src/commands/launch/crush.rs +++ b/crates/cli/src/commands/launch/crush.rs @@ -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!({ diff --git a/crates/cli/src/commands/launch/opencode.rs b/crates/cli/src/commands/launch/opencode.rs index 726f364..b08931a 100644 --- a/crates/cli/src/commands/launch/opencode.rs +++ b/crates/cli/src/commands/launch/opencode.rs @@ -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!({ From ba64bbd096fa19f4a869c3dfca6f46532a0ff834 Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Fri, 26 Jun 2026 09:18:42 +0200 Subject: [PATCH 5/8] Prefer server gateway URL over local override The org's console-configured gateway_api_url is now authoritative when set; the local override (EDGEE_API_URL / profile gateway_url) applies only as a fallback. Also print the resolved gateway URL on launch claude. --- crates/cli/src/commands/launch/claude.rs | 1 + crates/cli/src/commands/launch/mod.rs | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/commands/launch/claude.rs b/crates/cli/src/commands/launch/claude.rs index d55b66c..b0b9bb6 100644 --- a/crates/cli/src/commands/launch/claude.rs +++ b/crates/cli/src/commands/launch/claude.rs @@ -71,6 +71,7 @@ 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; + println!("edgee: gateway URL -> {gateway_url}"); let mut cmd = std::process::Command::new(util::resolve_binary("claude")); cmd.env("ANTHROPIC_BASE_URL", &gateway_url); cmd.env( diff --git a/crates/cli/src/commands/launch/mod.rs b/crates/cli/src/commands/launch/mod.rs index 40ba16a..d4034a4 100644 --- a/crates/cli/src/commands/launch/mod.rs +++ b/crates/cli/src/commands/launch/mod.rs @@ -90,16 +90,14 @@ async fn print_session_stats( /// Resolves the gateway base URL for a launch. /// -/// Precedence (highest first): an explicit local override (`EDGEE_API_URL` env -/// or the profile's `gateway_url`), then the org's console-configured -/// `gateway_api_url`, then the built-in default. The org fetch is best-effort: -/// any failure (offline, no org selected, transient error) falls through to the -/// next source so launch never breaks. +/// Precedence (highest first): the org's console-configured `gateway_api_url`, +/// then an explicit local override (`EDGEE_API_URL` env or the profile's +/// `gateway_url`), then the built-in default. The server value is authoritative +/// when set; the local override only applies as a fallback (offline, no org +/// selected, or the org has no configured gateway). The org fetch is +/// best-effort: any failure falls through to the next source so launch never +/// breaks. pub async fn resolve_gateway_base_url(creds: &crate::config::Credentials) -> String { - if let Some(local) = crate::config::gateway_url_local_override() { - return local; - } - 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()), @@ -113,6 +111,10 @@ pub async fn resolve_gateway_base_url(creds: &crate::config::Credentials) -> Str } } + if let Some(local) = crate::config::gateway_url_local_override() { + return local; + } + crate::config::DEFAULT_GATEWAY_URL.to_string() } From 1d2c3e5230931f630ef69e6b142619213c2b7dee Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Fri, 26 Jun 2026 09:21:53 +0200 Subject: [PATCH 6/8] Drop debug println of resolved gateway URL --- crates/cli/src/commands/launch/claude.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/cli/src/commands/launch/claude.rs b/crates/cli/src/commands/launch/claude.rs index b0b9bb6..d55b66c 100644 --- a/crates/cli/src/commands/launch/claude.rs +++ b/crates/cli/src/commands/launch/claude.rs @@ -71,7 +71,6 @@ 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; - println!("edgee: gateway URL -> {gateway_url}"); let mut cmd = std::process::Command::new(util::resolve_binary("claude")); cmd.env("ANTHROPIC_BASE_URL", &gateway_url); cmd.env( From 69cd7f92d7daa989d5a0fddfc8c81efc86207974 Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Fri, 26 Jun 2026 09:47:39 +0200 Subject: [PATCH 7/8] order --- crates/cli/src/commands/launch/mod.rs | 26 +++++++++++++++++--------- crates/cli/src/config.rs | 24 ++++++++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/crates/cli/src/commands/launch/mod.rs b/crates/cli/src/commands/launch/mod.rs index d4034a4..975a979 100644 --- a/crates/cli/src/commands/launch/mod.rs +++ b/crates/cli/src/commands/launch/mod.rs @@ -90,14 +90,22 @@ async fn print_session_stats( /// Resolves the gateway base URL for a launch. /// -/// Precedence (highest first): the org's console-configured `gateway_api_url`, -/// then an explicit local override (`EDGEE_API_URL` env or the profile's -/// `gateway_url`), then the built-in default. The server value is authoritative -/// when set; the local override only applies as a fallback (offline, no org -/// selected, or the org has no configured gateway). The org fetch is -/// best-effort: any failure falls through to the next source so launch never -/// breaks. +/// Precedence (highest first): +/// 1. `EDGEE_API_URL` env var — the explicit escape hatch (local debugging, +/// incident response). It outranks the server so an operator can always +/// force a value. +/// 2. The org's console-configured `gateway_api_url` — central, admin-managed +/// default for the normal case. +/// 3. The active profile's persisted `gateway_url`. +/// 4. The built-in default. +/// +/// 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(token), Some(org_id)) = ( creds.user_token.as_deref().filter(|t| !t.is_empty()), creds.org_id.as_deref().filter(|o| !o.is_empty()), @@ -111,8 +119,8 @@ pub async fn resolve_gateway_base_url(creds: &crate::config::Credentials) -> Str } } - if let Some(local) = crate::config::gateway_url_local_override() { - return local; + if let Some(profile_url) = crate::config::gateway_url_profile_override() { + return profile_url; } crate::config::DEFAULT_GATEWAY_URL.to_string() diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 6d9a913..4fb7e0c 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -282,16 +282,20 @@ pub fn console_api_base_url() -> String { /// org-configured gateway URL is available. pub const DEFAULT_GATEWAY_URL: &str = "https://api.edgee.ai"; -/// An explicit *local* gateway override, if one is set: the `EDGEE_API_URL` env -/// var (highest priority) or the active profile's `gateway_url`. The launch path -/// layers the org's console-configured gateway below this and above the default; -/// see `commands::launch::resolve_gateway_base_url`. -pub fn gateway_url_local_override() -> Option { - if let Ok(v) = std::env::var("EDGEE_API_URL") { - if !v.is_empty() { - return Some(v); - } - } +/// 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 { + std::env::var("EDGEE_API_URL") + .ok() + .filter(|s| !s.is_empty()) +} + +/// The active profile's persisted `gateway_url`, if set and non-empty. This is a +/// stored preference, so the launch path layers it *below* the org's +/// console-configured gateway and above the built-in default. +pub fn gateway_url_profile_override() -> Option { read() .ok() .and_then(|p| p.gateway_url) From 2603634d7a5a49d79754d1000e6e852cd2bf2e00 Mon Sep 17 00:00:00 2001 From: Clement Bouvet Date: Fri, 26 Jun 2026 09:53:40 +0200 Subject: [PATCH 8/8] order 2 --- crates/cli/src/commands/launch/mod.rs | 25 +++++++++++++------------ crates/cli/src/config.rs | 7 ++++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/cli/src/commands/launch/mod.rs b/crates/cli/src/commands/launch/mod.rs index 975a979..81d220a 100644 --- a/crates/cli/src/commands/launch/mod.rs +++ b/crates/cli/src/commands/launch/mod.rs @@ -91,21 +91,26 @@ async fn print_session_stats( /// Resolves the gateway base URL for a launch. /// /// Precedence (highest first): -/// 1. `EDGEE_API_URL` env var — the explicit escape hatch (local debugging, -/// incident response). It outranks the server so an operator can always -/// force a value. -/// 2. The org's console-configured `gateway_api_url` — central, admin-managed -/// default for the normal case. -/// 3. The active profile's persisted `gateway_url`. +/// 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. /// -/// 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). +/// 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()), @@ -119,10 +124,6 @@ pub async fn resolve_gateway_base_url(creds: &crate::config::Credentials) -> Str } } - if let Some(profile_url) = crate::config::gateway_url_profile_override() { - return profile_url; - } - crate::config::DEFAULT_GATEWAY_URL.to_string() } diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 4fb7e0c..9fac56e 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -292,9 +292,10 @@ pub fn gateway_url_env_override() -> Option { .filter(|s| !s.is_empty()) } -/// The active profile's persisted `gateway_url`, if set and non-empty. This is a -/// stored preference, so the launch path layers it *below* the org's -/// console-configured gateway and above the built-in default. +/// 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 { read() .ok()