pub const DEFAULT_API_BASE_URL: &str = "https://api.tinyhumans.ai";
pub const DEFAULT_STAGING_API_BASE_URL: &str = "https://staging-api.tinyhumans.ai";
pub const APP_ENV_VAR: &str = "OPENHUMAN_APP_ENV";
pub const VITE_APP_ENV_VAR: &str = "VITE_OPENHUMAN_APP_ENV";
pub const OPENHUMAN_INFERENCE_PATH: &str = "/openai/v1/chat/completions";
const LOCAL_AI_PORTS: &[u16] = &[11434, 8000, 8080, 1234, 8888];
pub fn effective_inference_url(
api_url_override: &Option<String>,
inference_url_override: &Option<String>,
) -> String {
if let Some(u) = non_empty_str(inference_url_override) {
return u.to_string();
}
api_url(
&effective_api_url(api_url_override),
OPENHUMAN_INFERENCE_PATH,
)
}
pub fn effective_api_url(api_url: &Option<String>) -> String {
if let Some(u) = non_empty_str(api_url) {
return normalize_api_base_url(u);
}
api_base_from_env()
.unwrap_or_else(|| default_api_base_url_for_env(app_env_from_env().as_deref()).to_string())
}
pub fn effective_backend_api_url(api_url: &Option<String>) -> String {
if let Some(u) = non_empty_str(api_url) {
let is_local_ai = looks_like_local_ai_endpoint(u);
let is_inference_provider = looks_like_inference_provider_endpoint(u);
let is_openhuman = looks_like_openhuman_backend_endpoint(u);
let is_cloud_inference =
crate::openhuman::config::schema::cloud_providers::endpoint_host(u).is_some_and(|h| {
crate::openhuman::config::schema::cloud_providers::host_is_builtin_cloud_provider(
&h,
)
});
tracing::debug!(
api_url = %redact_url_for_log(u),
is_local_ai,
is_inference_provider,
is_cloud_inference,
is_openhuman,
"[api/config] evaluating backend api_url override"
);
if (!is_local_ai && !is_inference_provider && !is_cloud_inference) || is_openhuman {
let normalized = normalize_backend_api_base_url(u);
tracing::trace!(
api_url = %redact_url_for_log(u),
normalized_url = %redact_url_for_log(&normalized),
"[api/config] using configured backend api_url override"
);
return normalized;
}
tracing::debug!(
api_url = %redact_url_for_log(u),
is_local_ai,
is_inference_provider,
is_cloud_inference,
"[api/config] override classified as inference endpoint (managed provider or builtin cloud host) — falling back to backend default chain"
);
warn_backend_url_fallback_once(u);
}
api_base_from_env()
.map(|u| normalize_backend_api_base_url(&u))
.unwrap_or_else(|| default_api_base_url_for_env(app_env_from_env().as_deref()).to_string())
}
pub fn looks_like_local_ai_endpoint(url: &str) -> bool {
let trimmed = url.trim();
if trimmed.is_empty() {
return false;
}
let parsed = match url::Url::parse(trimmed) {
Ok(u) => u,
Err(_) => return false,
};
let path = parsed.path();
if path.ends_with("/v1/chat/completions") || path.ends_with("/v1/completions") {
return true;
}
if !host_is_local(&parsed) {
return false;
}
let port_is_llm = parsed
.port()
.map(|p| LOCAL_AI_PORTS.contains(&p))
.unwrap_or(false);
let path_is_llm = path.starts_with("/v1/") || path == "/v1";
port_is_llm || path_is_llm
}
const INFERENCE_PROVIDER_DOMAINS: &[&str] = &[
"openrouter.ai",
"openmodel.ai",
"openai.com",
"anthropic.com",
"groq.com",
"mistral.ai",
"deepseek.com",
"together.ai",
"together.xyz",
"perplexity.ai",
"fireworks.ai",
"deepinfra.com",
"anyscale.com",
"novita.ai",
"hyperbolic.xyz",
"x.ai",
"googleapis.com",
"cohere.ai",
"cohere.com",
];
pub fn looks_like_inference_provider_endpoint(url: &str) -> bool {
let trimmed = url.trim();
if trimmed.is_empty() {
return false;
}
if looks_like_openhuman_backend_endpoint(trimmed) {
return false;
}
let parsed = match url::Url::parse(trimmed) {
Ok(u) => u,
Err(_) => return false,
};
if let Some(host) = parsed.host_str() {
let host = host.to_ascii_lowercase();
if INFERENCE_PROVIDER_DOMAINS
.iter()
.any(|d| host == *d || host.ends_with(&format!(".{d}")))
{
return true;
}
}
let path = parsed.path().trim_end_matches('/');
if path == "/api/v1" || path == "/v1" {
return true;
}
false
}
fn looks_like_openhuman_backend_endpoint(url: &str) -> bool {
let trimmed = url.trim();
let redacted = redact_url_for_log(trimmed);
let parsed = match url::Url::parse(trimmed) {
Ok(p) => {
tracing::trace!(
api_url = %redacted,
"[api/config] parsed api_url for OpenHuman backend classification"
);
p
}
Err(e) => {
tracing::trace!(
api_url = %redacted,
error = %e,
"[api/config] api_url parse failed during OpenHuman backend classification"
);
return false;
}
};
let Some(host) = parsed.host_str().map(str::to_ascii_lowercase) else {
tracing::trace!(
api_url = %redacted,
"[api/config] api_url has no host — not classified as OpenHuman backend"
);
return false;
};
let is_openhuman = matches!(
host.as_str(),
"api.tinyhumans.ai" | "staging-api.tinyhumans.ai"
);
tracing::debug!(
api_url = %redacted,
host = %host,
is_openhuman,
"[api/config] OpenHuman backend classification complete"
);
is_openhuman
}
pub fn normalize_api_base_url(url: &str) -> String {
url.trim().trim_end_matches('/').to_string()
}
pub(crate) fn normalize_backend_api_base_url(url: &str) -> String {
let normalized = normalize_api_base_url(url);
if normalized.is_empty() {
return normalized;
}
let parsed =
url::Url::parse(&normalized).or_else(|_| url::Url::parse(&format!("https://{normalized}")));
let Ok(mut parsed) = parsed else {
return normalized;
};
if parsed.path() != "/" {
parsed.set_path("");
}
parsed.set_query(None);
parsed.set_fragment(None);
parsed.to_string().trim_end_matches('/').to_string()
}
pub fn api_url(base: &str, path: &str) -> String {
let base = base.trim();
if path.is_empty() {
return normalize_api_base_url(base);
}
match url::Url::parse(base) {
Ok(parsed) => match parsed.join(path) {
Ok(joined) => joined.to_string().trim_end_matches('/').to_string(),
Err(_) => fallback_concat(base, path),
},
Err(_) => fallback_concat(base, path),
}
}
#[inline]
fn fallback_concat(base: &str, path: &str) -> String {
let base = base.trim_end_matches('/');
if path.starts_with('/') {
format!("{base}{path}")
} else {
format!("{base}/{path}")
}
}
pub fn api_base_from_env() -> Option<String> {
for key in ["BACKEND_URL", "VITE_BACKEND_URL"] {
if let Ok(v) = std::env::var(key) {
let url = normalize_api_base_url(&v);
if !url.is_empty() {
return Some(url);
}
}
}
for v in compile_time_api_base_env_values().into_iter().flatten() {
let url = normalize_api_base_url(v);
if !url.is_empty() {
return Some(url);
}
}
None
}
pub fn app_env_from_env() -> Option<String> {
for key in [APP_ENV_VAR, VITE_APP_ENV_VAR] {
if let Ok(v) = std::env::var(key) {
let s = v.trim().to_ascii_lowercase();
if !s.is_empty() {
return Some(s);
}
}
}
for v in compile_time_app_env_values().into_iter().flatten() {
let s = v.trim().to_ascii_lowercase();
if !s.is_empty() {
return Some(s);
}
}
None
}
pub fn is_staging_app_env(app_env: Option<&str>) -> bool {
matches!(app_env.map(str::trim), Some(env) if env.eq_ignore_ascii_case("staging"))
}
pub fn default_api_base_url_for_env(app_env: Option<&str>) -> &'static str {
if is_staging_app_env(app_env) {
DEFAULT_STAGING_API_BASE_URL
} else {
DEFAULT_API_BASE_URL
}
}
#[cfg(not(test))]
fn compile_time_api_base_env_values() -> [Option<&'static str>; 2] {
[option_env!("BACKEND_URL"), option_env!("VITE_BACKEND_URL")]
}
#[cfg(test)]
fn compile_time_api_base_env_values() -> [Option<&'static str>; 2] {
[None, None]
}
#[cfg(not(test))]
fn compile_time_app_env_values() -> [Option<&'static str>; 2] {
[
option_env!("OPENHUMAN_APP_ENV"),
option_env!("VITE_OPENHUMAN_APP_ENV"),
]
}
#[cfg(test)]
fn compile_time_app_env_values() -> [Option<&'static str>; 2] {
[None, None]
}
pub(crate) fn redact_url_for_log(raw: &str) -> String {
let trimmed = raw.trim();
let parsed =
url::Url::parse(trimmed).or_else(|_| url::Url::parse(&format!("http://{trimmed}")));
let Ok(mut parsed) = parsed else {
return trimmed.to_string();
};
if !parsed.username().is_empty() {
let _ = parsed.set_username("redacted");
}
if parsed.password().is_some() {
let _ = parsed.set_password(Some("redacted"));
}
parsed.to_string().trim_end_matches('/').to_string()
}
fn warn_backend_url_fallback_once(local_url: &str) {
use std::sync::Once;
static WARNED: Once = Once::new();
WARNED.call_once(|| {
tracing::warn!(
local_url = %redact_url_for_log(local_url),
"[api/config] config.api_url looks like a local-AI endpoint; \
integrations base will fall back to env/default backend so \
/agent-integrations/* requests don't 404 against your local LLM"
);
});
}
#[inline]
fn non_empty_str(s: &Option<String>) -> Option<&str> {
s.as_deref().map(str::trim).filter(|s| !s.is_empty())
}
#[inline]
fn host_is_local(parsed: &url::Url) -> bool {
match parsed.host() {
Some(url::Host::Ipv4(addr)) => {
addr.is_loopback() || addr.is_unspecified() || addr.is_private()
}
Some(url::Host::Ipv6(addr)) => addr.is_loopback() || addr.is_unspecified(),
Some(url::Host::Domain(name)) => {
let h = name.to_ascii_lowercase();
h == "localhost" || h.ends_with(".localhost")
}
None => false,
}
}
#[cfg(test)]
mod tests {
use std::sync::{Mutex, MutexGuard, OnceLock};
use super::*;
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn env_lock() -> MutexGuard<'static, ()> {
match ENV_LOCK.get_or_init(Mutex::default).lock() {
Ok(g) => g,
Err(p) => p.into_inner(), }
}
struct EnvSnapshot {
vars: [(&'static str, Option<String>); 4],
}
impl EnvSnapshot {
fn clear_backend_env() -> Self {
let keys = [
"BACKEND_URL",
"VITE_BACKEND_URL",
APP_ENV_VAR,
VITE_APP_ENV_VAR,
];
let vars = keys.map(|k| (k, std::env::var(k).ok()));
for (k, _) in &vars {
std::env::remove_var(k);
}
Self { vars }
}
}
impl Drop for EnvSnapshot {
fn drop(&mut self) {
for (key, value) in &self.vars {
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
}
fn fallback_backend_base_for_current_build() -> String {
api_base_from_env().unwrap_or_else(|| {
default_api_base_url_for_env(app_env_from_env().as_deref()).to_string()
})
}
#[test]
fn api_url_empty_path_returns_normalized_base() {
assert_eq!(
api_url("https://api.tinyhumans.ai", ""),
"https://api.tinyhumans.ai"
);
assert_eq!(
api_url("https://api.tinyhumans.ai/", ""),
"https://api.tinyhumans.ai"
);
assert_eq!(
api_url(" https://api.tinyhumans.ai/ ", ""),
"https://api.tinyhumans.ai"
);
}
#[test]
fn api_url_absolute_path_replaces_base_path() {
assert_eq!(
api_url(
"https://api.tinyhumans.ai/openai/v1/chat/completions",
"/agent-integrations/composio/toolkits",
),
"https://api.tinyhumans.ai/agent-integrations/composio/toolkits"
);
}
#[test]
fn api_url_clean_base_joins_cleanly() {
let expected = "https://api.tinyhumans.ai/agent-integrations/composio/toolkits";
assert_eq!(
api_url(
"https://api.tinyhumans.ai",
"/agent-integrations/composio/toolkits"
),
expected
);
assert_eq!(
api_url(
"https://api.tinyhumans.ai/",
"/agent-integrations/composio/toolkits"
),
expected
);
}
#[test]
fn api_url_preserves_query_string_on_path() {
assert_eq!(
api_url(
"https://api.tinyhumans.ai",
"/agent-integrations/composio/tools?toolkits=gmail"
),
"https://api.tinyhumans.ai/agent-integrations/composio/tools?toolkits=gmail"
);
}
#[test]
fn api_url_unparseable_base_falls_back_to_concat() {
assert_eq!(api_url("not a url", "/x"), "not a url/x");
assert_eq!(api_url("not a url/", "/x"), "not a url/x");
}
#[test]
fn api_url_with_lm_studio_base_joins_correctly() {
assert_eq!(
api_url("http://localhost:1234/v1", "/agent-integrations/foo"),
"http://localhost:1234/agent-integrations/foo"
);
}
#[test]
fn api_url_multiple_trailing_slashes_on_base_are_stripped() {
assert_eq!(
api_url("https://api.tinyhumans.ai///", "/v1/foo"),
"https://api.tinyhumans.ai/v1/foo"
);
}
#[test]
fn api_url_relative_path_without_leading_slash_does_not_panic() {
assert!(!api_url("https://api.tinyhumans.ai", "relative").is_empty());
}
#[test]
fn normalize_strips_trailing_slashes_and_whitespace() {
assert_eq!(
normalize_api_base_url("https://api.tinyhumans.ai/"),
"https://api.tinyhumans.ai"
);
assert_eq!(
normalize_api_base_url("https://api.tinyhumans.ai///"),
"https://api.tinyhumans.ai"
);
assert_eq!(
normalize_api_base_url(" https://api.tinyhumans.ai "),
"https://api.tinyhumans.ai"
);
assert_eq!(
normalize_api_base_url(" https://api.tinyhumans.ai/ "),
"https://api.tinyhumans.ai"
);
}
#[test]
fn normalize_preserves_mid_path() {
assert_eq!(
normalize_api_base_url("https://api.tinyhumans.ai/v2"),
"https://api.tinyhumans.ai/v2"
);
}
#[test]
fn normalize_empty_string_returns_empty() {
assert_eq!(normalize_api_base_url(""), "");
}
#[test]
fn normalize_backend_strips_inference_path() {
assert_eq!(
normalize_backend_api_base_url("https://api.tinyhumans.ai/openai/v1/chat/completions"),
"https://api.tinyhumans.ai"
);
}
#[test]
fn normalize_backend_handles_schemeless_input() {
assert_eq!(
normalize_backend_api_base_url("api.tinyhumans.ai/openai/v1/chat/completions"),
"https://api.tinyhumans.ai"
);
}
#[test]
fn normalize_backend_passes_through_clean_root() {
assert_eq!(
normalize_backend_api_base_url("https://api.tinyhumans.ai/"),
"https://api.tinyhumans.ai"
);
}
#[test]
fn normalize_backend_empty_string_is_idempotent() {
assert_eq!(normalize_backend_api_base_url(""), "");
}
#[test]
fn staging_env_resolves_to_staging_url() {
assert_eq!(
default_api_base_url_for_env(Some("staging")),
DEFAULT_STAGING_API_BASE_URL
);
assert!(is_staging_app_env(Some("STAGING")));
}
#[test]
fn non_staging_env_resolves_to_production_url() {
assert_eq!(
default_api_base_url_for_env(Some("production")),
DEFAULT_API_BASE_URL
);
assert_eq!(default_api_base_url_for_env(None), DEFAULT_API_BASE_URL);
assert!(!is_staging_app_env(Some("development")));
}
#[test]
fn app_env_from_env_reads_runtime_var() {
let _guard = env_lock();
let prev = std::env::var(APP_ENV_VAR).ok();
std::env::set_var(APP_ENV_VAR, "staging");
let result = app_env_from_env();
match prev {
Some(v) => std::env::set_var(APP_ENV_VAR, v),
None => std::env::remove_var(APP_ENV_VAR),
}
assert_eq!(result.as_deref(), Some("staging"));
}
#[test]
fn app_env_empty_primary_falls_through_to_secondary() {
let _guard = env_lock();
let prev_p = std::env::var(APP_ENV_VAR).ok();
let prev_s = std::env::var(VITE_APP_ENV_VAR).ok();
std::env::set_var(APP_ENV_VAR, "");
std::env::set_var(VITE_APP_ENV_VAR, "staging");
let result = app_env_from_env();
match prev_p {
Some(v) => std::env::set_var(APP_ENV_VAR, v),
None => std::env::remove_var(APP_ENV_VAR),
}
match prev_s {
Some(v) => std::env::set_var(VITE_APP_ENV_VAR, v),
None => std::env::remove_var(VITE_APP_ENV_VAR),
}
assert_eq!(result.as_deref(), Some("staging"));
}
#[test]
fn api_base_from_env_reads_runtime_var() {
let _guard = env_lock();
let prev = std::env::var("BACKEND_URL").ok();
std::env::set_var("BACKEND_URL", "https://staging-api.tinyhumans.ai/");
let result = api_base_from_env();
match prev {
Some(v) => std::env::set_var("BACKEND_URL", v),
None => std::env::remove_var("BACKEND_URL"),
}
assert_eq!(result.as_deref(), Some("https://staging-api.tinyhumans.ai"));
}
#[test]
fn api_base_empty_primary_falls_through_to_secondary() {
let _guard = env_lock();
let prev_p = std::env::var("BACKEND_URL").ok();
let prev_s = std::env::var("VITE_BACKEND_URL").ok();
std::env::set_var("BACKEND_URL", "");
std::env::set_var("VITE_BACKEND_URL", "https://staging-api.tinyhumans.ai/");
let result = api_base_from_env();
match prev_p {
Some(v) => std::env::set_var("BACKEND_URL", v),
None => std::env::remove_var("BACKEND_URL"),
}
match prev_s {
Some(v) => std::env::set_var("VITE_BACKEND_URL", v),
None => std::env::remove_var("VITE_BACKEND_URL"),
}
assert_eq!(result.as_deref(), Some("https://staging-api.tinyhumans.ai"));
}
#[test]
fn local_ai_matches_loopback_hosts() {
assert!(looks_like_local_ai_endpoint("http://127.0.0.1:11434/v1"));
assert!(looks_like_local_ai_endpoint(
"http://127.0.0.1:8080/v1/chat/completions"
));
assert!(looks_like_local_ai_endpoint("http://localhost:11434/v1"));
assert!(looks_like_local_ai_endpoint("http://[::1]:11434"));
assert!(looks_like_local_ai_endpoint("http://0.0.0.0:11434/v1"));
}
#[test]
fn local_ai_matches_chat_completions_path_on_any_host() {
assert!(looks_like_local_ai_endpoint(
"http://203.0.113.5:8080/v1/chat/completions"
));
assert!(looks_like_local_ai_endpoint(
"https://my-ollama.example/v1/completions"
));
}
#[test]
fn local_ai_rejects_bare_loopback_with_random_port() {
assert!(!looks_like_local_ai_endpoint("http://127.0.0.1:54321"));
assert!(!looks_like_local_ai_endpoint("http://127.0.0.1:42000/"));
assert!(!looks_like_local_ai_endpoint("http://localhost:33333"));
assert!(!looks_like_local_ai_endpoint("http://[::1]:51234"));
}
#[test]
fn local_ai_matches_private_lan_hosts() {
assert!(looks_like_local_ai_endpoint(
"http://192.168.1.100:11434/v1"
));
assert!(looks_like_local_ai_endpoint("http://10.0.0.5:8080/v1"));
assert!(looks_like_local_ai_endpoint("http://172.16.0.42:8000"));
}
#[test]
fn local_ai_rejects_real_backends() {
assert!(!looks_like_local_ai_endpoint("https://api.tinyhumans.ai"));
assert!(!looks_like_local_ai_endpoint(
"https://staging-api.tinyhumans.ai"
));
assert!(!looks_like_local_ai_endpoint("https://api.openai.com/v1"));
assert!(!looks_like_local_ai_endpoint(
"https://my-backend.example/v1"
));
}
#[test]
fn local_ai_rejects_substring_path_false_positives() {
assert!(!looks_like_local_ai_endpoint(
"https://real-backend.example/audit/v1/chat/completions-logs"
));
assert!(!looks_like_local_ai_endpoint(
"https://real-backend.example/v1/chat/completions/history"
));
assert!(!looks_like_local_ai_endpoint(
"https://real-backend.example/v1/completions-archive"
));
}
#[test]
fn local_ai_handles_garbage_input() {
assert!(!looks_like_local_ai_endpoint(""));
assert!(!looks_like_local_ai_endpoint(" "));
assert!(!looks_like_local_ai_endpoint("not a url"));
assert!(!looks_like_local_ai_endpoint("/v1/chat/completions")); }
#[test]
fn local_ai_matches_lm_studio_default_port() {
assert!(looks_like_local_ai_endpoint("http://localhost:1234"));
assert!(looks_like_local_ai_endpoint("http://127.0.0.1:1234"));
assert!(looks_like_local_ai_endpoint(
"http://127.0.0.1:1234/v1/chat/completions"
));
}
#[test]
fn local_ai_matches_v1_subpath_on_loopback() {
assert!(looks_like_local_ai_endpoint(
"http://localhost:11434/v1/models"
));
assert!(looks_like_local_ai_endpoint(
"http://127.0.0.1:8080/v1/embeddings"
));
}
#[test]
fn openhuman_backend_detection_accepts_hosted_api_paths() {
assert!(looks_like_openhuman_backend_endpoint(
"https://api.tinyhumans.ai/openai/v1/chat/completions"
));
assert!(looks_like_openhuman_backend_endpoint(
"https://staging-api.tinyhumans.ai/openai/v1/chat/completions"
));
assert!(!looks_like_openhuman_backend_endpoint(
"https://openrouter.ai/api/v1/chat/completions"
));
assert!(!looks_like_openhuman_backend_endpoint(
"http://localhost:1234/v1/chat/completions"
));
}
#[test]
fn backend_url_handles_llm_endpoint_overrides() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
let fallback = fallback_backend_base_for_current_build();
let cases: &[(&str, &str)] = &[
(
"https://api.tinyhumans.ai/openai/v1/chat/completions",
"https://api.tinyhumans.ai",
),
("http://localhost:11434/v1/chat/completions", &fallback),
("https://api.tinyhumans.ai", "https://api.tinyhumans.ai"),
(
"https://api.tinyhumans.ai/openai/v1/",
"https://api.tinyhumans.ai",
),
("https://openrouter.ai/api/v1/chat/completions", &fallback),
];
for (api_url, expected) in cases {
assert_eq!(
effective_backend_api_url(&Some(api_url.to_string())),
*expected,
"api_url = {api_url}"
);
}
}
#[test]
fn backend_url_falls_back_for_local_ai_override() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
let expected = fallback_backend_base_for_current_build();
assert_eq!(
effective_backend_api_url(&Some("http://127.0.0.1:11434/v1".to_string())),
expected
);
}
#[test]
fn backend_url_falls_back_for_cloud_inference_base() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
let fallback = fallback_backend_base_for_current_build();
let falls_back: &[&str] = &[
"https://openrouter.ai/api/v1",
"https://api.openai.com/v1",
"https://api.groq.com/openai/v1",
"https://generativelanguage.googleapis.com/v1beta/openai",
];
for api_url in falls_back {
assert_eq!(
effective_backend_api_url(&Some(api_url.to_string())),
fallback,
"cloud inference base must fall back: {api_url}"
);
}
assert_eq!(
effective_backend_api_url(&Some("https://api.tinyhumans.ai/v1".to_string())),
"https://api.tinyhumans.ai",
"openhuman backend host must pass through"
);
assert_eq!(
effective_backend_api_url(&Some("https://my-backend.example/v1".to_string())),
fallback,
"unknown bare-/v1 base is an inference base and must fall back"
);
}
#[test]
fn backend_url_falls_back_to_env_when_override_is_local_ai() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
std::env::set_var("BACKEND_URL", "https://staging-api.tinyhumans.ai/");
assert_eq!(
effective_backend_api_url(&Some(
"http://127.0.0.1:8080/v1/chat/completions".to_string()
)),
"https://staging-api.tinyhumans.ai"
);
}
#[test]
fn backend_url_keeps_real_backend_override() {
assert_eq!(
effective_backend_api_url(&Some("https://staging-api.tinyhumans.ai/".to_string())),
"https://staging-api.tinyhumans.ai"
);
}
#[test]
fn backend_url_without_override_matches_effective_api_url() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
assert_eq!(effective_backend_api_url(&None), effective_api_url(&None));
}
#[test]
fn inference_provider_matches_known_remote_hosts() {
assert!(looks_like_inference_provider_endpoint(
"https://openrouter.ai/api/v1"
));
assert!(looks_like_inference_provider_endpoint(
"https://api.openmodel.ai/v1"
));
assert!(looks_like_inference_provider_endpoint(
"https://api.openai.com/v1"
));
assert!(looks_like_inference_provider_endpoint(
"https://api.groq.com/openai/v1"
));
assert!(looks_like_inference_provider_endpoint(
"https://api.mistral.ai"
));
}
#[test]
fn inference_provider_matches_bare_v1_base_on_unknown_host() {
assert!(looks_like_inference_provider_endpoint(
"https://llm.unknown-provider.example/v1"
));
assert!(looks_like_inference_provider_endpoint(
"https://gw.example.test/api/v1/"
));
}
#[test]
fn inference_provider_excludes_openhuman_backend_and_plain_hosts() {
assert!(!looks_like_inference_provider_endpoint(
"https://api.tinyhumans.ai/openai/v1/chat/completions"
));
assert!(!looks_like_inference_provider_endpoint(
"https://staging-api.tinyhumans.ai/"
));
assert!(!looks_like_inference_provider_endpoint(
"https://my-openhuman.example.com/"
));
assert!(!looks_like_inference_provider_endpoint(""));
assert!(!looks_like_inference_provider_endpoint("not a url"));
}
#[test]
fn backend_url_falls_back_for_remote_inference_provider_override() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
let expected = fallback_backend_base_for_current_build();
assert_eq!(
effective_backend_api_url(&Some("https://openrouter.ai/api/v1".to_string())),
expected
);
assert_eq!(
effective_backend_api_url(&Some("https://api.openmodel.ai/v1".to_string())),
expected
);
}
#[test]
fn backend_url_falls_back_to_env_for_remote_inference_provider() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
std::env::set_var("BACKEND_URL", "https://staging-api.tinyhumans.ai/");
assert_eq!(
effective_backend_api_url(&Some("https://openrouter.ai/api/v1".to_string())),
"https://staging-api.tinyhumans.ai"
);
}
#[test]
fn backend_url_strips_inference_path_from_env() {
let _guard = env_lock();
let _env = EnvSnapshot::clear_backend_env();
std::env::set_var(
"BACKEND_URL",
"https://api.tinyhumans.ai/openai/v1/chat/completions",
);
assert_eq!(
effective_backend_api_url(&None),
"https://api.tinyhumans.ai"
);
}
}