mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix: keep custom cloud provider as default (#2142)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
ba40be1973
commit
c07e8226fd
@@ -809,7 +809,7 @@ pub(super) fn redact_url_for_log(raw: &str) -> String {
|
||||
/// untouched. Routing fields that already contain a `:` are assumed to be
|
||||
/// in the new `<slug>:<model>` form.
|
||||
fn migrate_cloud_provider_slugs(config: &mut Config) {
|
||||
use super::cloud_providers::migrate_legacy_fields;
|
||||
use super::cloud_providers::{migrate_legacy_fields, AuthStyle};
|
||||
|
||||
// Step 1: migrate every cloud_providers entry in-place.
|
||||
for entry in &mut config.cloud_providers {
|
||||
@@ -826,6 +826,23 @@ fn migrate_cloud_provider_slugs(config: &mut Config) {
|
||||
.map(|e| (e.slug.clone(), e.id.clone()))
|
||||
.collect();
|
||||
|
||||
let legacy_custom_slug = config
|
||||
.inference_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|url| !url.is_empty() && !looks_like_openhuman_provider_endpoint(url))
|
||||
.and_then(|url| {
|
||||
let normalized = normalize_provider_endpoint(url);
|
||||
config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
!is_openhuman_provider_entry(entry)
|
||||
&& normalize_provider_endpoint(&entry.endpoint) == normalized
|
||||
})
|
||||
.map(|entry| entry.slug.clone())
|
||||
});
|
||||
|
||||
// Helper: rewrite a single routing field.
|
||||
// Legacy bare strings are: "cloud", "openhuman", "openai", "anthropic",
|
||||
// "openrouter", "custom" (no ':'). New strings contain ':'.
|
||||
@@ -841,7 +858,10 @@ fn migrate_cloud_provider_slugs(config: &mut Config) {
|
||||
match raw.as_str() {
|
||||
"cloud" => {
|
||||
// "cloud" sentinel: look for the primary or first non-openhuman entry.
|
||||
// If none found, leave as "openhuman".
|
||||
// If a legacy external inference_url exists and primary still points
|
||||
// at OpenHuman, keep routing on that custom provider; that shape was
|
||||
// written by older builds that preserved the endpoint but defaulted
|
||||
// primary_cloud to OpenHuman.
|
||||
let primary_slug = config.primary_cloud.as_deref().and_then(|pid| {
|
||||
config
|
||||
.cloud_providers
|
||||
@@ -849,18 +869,29 @@ fn migrate_cloud_provider_slugs(config: &mut Config) {
|
||||
.find(|e| e.id == pid)
|
||||
.map(|e| e.slug.clone())
|
||||
});
|
||||
let slug = primary_slug.or_else(|| {
|
||||
config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
.find(|e| e.slug != "openhuman")
|
||||
.map(|e| e.slug.clone())
|
||||
});
|
||||
let slug = match primary_slug.as_deref() {
|
||||
Some("openhuman") => legacy_custom_slug.clone().or(primary_slug),
|
||||
Some(_) => primary_slug,
|
||||
None => legacy_custom_slug.clone().or_else(|| {
|
||||
config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
.find(|entry| !is_openhuman_provider_entry(entry))
|
||||
.map(|entry| entry.slug.clone())
|
||||
}),
|
||||
};
|
||||
if let Some(s) = slug {
|
||||
tracing::info!(
|
||||
"[config][migrate] rewriting routing 'cloud' → '{s}:' (empty model)"
|
||||
);
|
||||
*field = Some(format!("{s}:"));
|
||||
if s == "openhuman" {
|
||||
tracing::debug!(
|
||||
"[config][migrate] rewriting routing 'cloud' → 'openhuman'"
|
||||
);
|
||||
*field = Some("openhuman".to_string());
|
||||
} else {
|
||||
tracing::info!(
|
||||
"[config][migrate] rewriting routing 'cloud' → '{s}:' (empty model)"
|
||||
);
|
||||
*field = Some(format!("{s}:"));
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"[config][migrate] routing 'cloud' with no non-openhuman provider → 'openhuman'"
|
||||
@@ -897,6 +928,29 @@ fn migrate_cloud_provider_slugs(config: &mut Config) {
|
||||
rewrite(&mut config.heartbeat_provider);
|
||||
rewrite(&mut config.learning_provider);
|
||||
rewrite(&mut config.subconscious_provider);
|
||||
|
||||
fn normalize_provider_endpoint(url: &str) -> String {
|
||||
url.trim().trim_end_matches('/').to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn looks_like_openhuman_provider_endpoint(url: &str) -> bool {
|
||||
let lower = url.trim().to_ascii_lowercase();
|
||||
let without_scheme = lower.split("://").nth(1).unwrap_or(&lower);
|
||||
let authority = without_scheme.split('/').next().unwrap_or("");
|
||||
let host = authority.split('@').next_back().unwrap_or(authority);
|
||||
let host_no_port = host.split(':').next().unwrap_or(host);
|
||||
matches!(
|
||||
host_no_port,
|
||||
"api.openhuman.ai" | "api.tinyhumans.ai" | "staging-api.tinyhumans.ai" | "openhuman"
|
||||
) || host_no_port.ends_with(".openhuman.ai")
|
||||
|| host_no_port.ends_with(".tinyhumans.ai")
|
||||
}
|
||||
|
||||
fn is_openhuman_provider_entry(entry: &super::cloud_providers::CloudProviderCreds) -> bool {
|
||||
entry.slug == "openhuman"
|
||||
|| matches!(entry.auth_style, AuthStyle::OpenhumanJwt)
|
||||
|| looks_like_openhuman_provider_endpoint(&entry.endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_legacy_autocomplete_disabled_apps(config: &mut Config) {
|
||||
|
||||
@@ -1407,6 +1407,92 @@ fn migrate_legacy_inference_url_is_noop_when_inference_url_set() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_cloud_provider_slugs_routes_cloud_to_legacy_custom_when_primary_is_openhuman() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.inference_url = Some("https://api.example.com/v1".into());
|
||||
cfg.primary_cloud = Some("p_oh".into());
|
||||
cfg.memory_provider = Some("cloud".into());
|
||||
cfg.reasoning_provider = Some("openhuman".into());
|
||||
cfg.cloud_providers = vec![
|
||||
crate::openhuman::config::schema::CloudProviderCreds {
|
||||
id: "p_oh".into(),
|
||||
slug: "openhuman".into(),
|
||||
label: "OpenHuman".into(),
|
||||
endpoint: "https://api.openhuman.ai/v1".into(),
|
||||
auth_style: crate::openhuman::config::schema::AuthStyle::OpenhumanJwt,
|
||||
..Default::default()
|
||||
},
|
||||
crate::openhuman::config::schema::CloudProviderCreds {
|
||||
id: "p_custom".into(),
|
||||
slug: "custom".into(),
|
||||
label: "Custom".into(),
|
||||
endpoint: "https://api.example.com/v1/".into(),
|
||||
auth_style: crate::openhuman::config::schema::AuthStyle::Bearer,
|
||||
default_model: Some("gpt-4o-mini".into()),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
migrate_cloud_provider_slugs(&mut cfg);
|
||||
|
||||
assert_eq!(cfg.memory_provider.as_deref(), Some("custom:"));
|
||||
assert_eq!(
|
||||
cfg.reasoning_provider.as_deref(),
|
||||
Some("openhuman"),
|
||||
"explicit OpenHuman routing must stay explicit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_cloud_provider_slugs_keeps_cloud_on_openhuman_without_legacy_custom() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.primary_cloud = Some("p_oh".into());
|
||||
cfg.memory_provider = Some("cloud".into());
|
||||
cfg.cloud_providers = vec![crate::openhuman::config::schema::CloudProviderCreds {
|
||||
id: "p_oh".into(),
|
||||
slug: "openhuman".into(),
|
||||
label: "OpenHuman".into(),
|
||||
endpoint: "https://api.tinyhumans.ai/v1".into(),
|
||||
auth_style: crate::openhuman::config::schema::AuthStyle::OpenhumanJwt,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
migrate_cloud_provider_slugs(&mut cfg);
|
||||
|
||||
assert_eq!(cfg.memory_provider.as_deref(), Some("openhuman"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_cloud_provider_slugs_does_not_pick_unmatched_custom_provider() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.inference_url = Some("https://api.example.com/v1".into());
|
||||
cfg.primary_cloud = Some("p_oh".into());
|
||||
cfg.memory_provider = Some("cloud".into());
|
||||
cfg.cloud_providers = vec![
|
||||
crate::openhuman::config::schema::CloudProviderCreds {
|
||||
id: "p_oh".into(),
|
||||
slug: "openhuman".into(),
|
||||
label: "OpenHuman".into(),
|
||||
endpoint: "https://api.openhuman.ai/v1".into(),
|
||||
auth_style: crate::openhuman::config::schema::AuthStyle::OpenhumanJwt,
|
||||
..Default::default()
|
||||
},
|
||||
crate::openhuman::config::schema::CloudProviderCreds {
|
||||
id: "p_other".into(),
|
||||
slug: "other".into(),
|
||||
label: "Other".into(),
|
||||
endpoint: "https://other.example.com/v1".into(),
|
||||
auth_style: crate::openhuman::config::schema::AuthStyle::Bearer,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
migrate_cloud_provider_slugs(&mut cfg);
|
||||
|
||||
assert_eq!(cfg.memory_provider.as_deref(), Some("openhuman"));
|
||||
}
|
||||
|
||||
/// Regression test for #1900: secrets are encrypted on save and decrypted on load.
|
||||
///
|
||||
/// Verifies that:
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
//!
|
||||
//! ```text
|
||||
//! "openhuman" → OpenHumanBackendProvider; model = config.default_model
|
||||
//! "cloud" / missing → primary_cloud; legacy custom inference_url wins when
|
||||
//! primary still points at OpenHuman after migration
|
||||
//! "ollama:<model>[@<temp>]" → local Ollama at config.local_ai.base_url
|
||||
//! "<slug>:<model>[@<temp>]" → cloud_providers entry keyed by slug;
|
||||
//! builds OpenAiCompatibleProvider (Bearer) or
|
||||
//! Anthropic flavour depending on auth_style.
|
||||
//! "" / missing → falls back to "openhuman"
|
||||
//! ```
|
||||
//!
|
||||
//! The optional `@<temp>` suffix pins a per-workload temperature override on
|
||||
@@ -45,7 +46,11 @@ pub fn auth_key_for_slug(slug: &str) -> String {
|
||||
|
||||
/// Return the configured provider string for a named workload role.
|
||||
///
|
||||
/// Returns `"openhuman"` when the workload has no explicit override.
|
||||
/// Empty / `"cloud"` resolves through `primary_cloud`. For backwards
|
||||
/// compatibility, a legacy external `inference_url` takes precedence when
|
||||
/// `primary_cloud` still points at OpenHuman because migration 1→2 preserved
|
||||
/// the URL as a custom provider entry but older configs did not explicitly set
|
||||
/// per-workload routes.
|
||||
pub fn provider_for_role(role: &str, config: &Config) -> String {
|
||||
let opt = match role {
|
||||
"chat" => config.chat_provider.as_deref(),
|
||||
@@ -66,24 +71,7 @@ pub fn provider_for_role(role: &str, config: &Config) -> String {
|
||||
};
|
||||
let s = opt.unwrap_or("").trim();
|
||||
if s.is_empty() || s == "cloud" {
|
||||
// When no explicit per-workload provider is set, resolve
|
||||
// primary_cloud. If it points to a non-openhuman entry, route
|
||||
// there so users can use their own LLM provider without having
|
||||
// to set every single workload knob. (An active app-session
|
||||
// is still required — verified inside
|
||||
// create_chat_provider_from_string.)
|
||||
let primary_slug = config.primary_cloud.as_deref().and_then(|pid| {
|
||||
config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
.find(|e| e.id == pid && e.slug != "openhuman")
|
||||
.map(|e| e.slug.clone())
|
||||
});
|
||||
if let Some(slug) = primary_slug {
|
||||
format!("{slug}:")
|
||||
} else {
|
||||
PROVIDER_OPENHUMAN.to_string()
|
||||
}
|
||||
resolve_primary_cloud_provider_string(config)
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
@@ -118,9 +106,10 @@ pub fn create_chat_provider_from_string(
|
||||
p
|
||||
);
|
||||
|
||||
// Empty / legacy "cloud" sentinel → OpenHuman backend.
|
||||
// Empty / legacy "cloud" sentinel → primary cloud target.
|
||||
if p.is_empty() || p == "cloud" {
|
||||
return make_openhuman_backend(config);
|
||||
let resolved = resolve_primary_cloud_provider_string(config);
|
||||
return create_chat_provider_from_string(role, &resolved, config);
|
||||
}
|
||||
|
||||
if p == PROVIDER_OPENHUMAN {
|
||||
@@ -268,6 +257,98 @@ fn verify_session_active(config: &Config) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_primary_cloud_provider_string(config: &Config) -> String {
|
||||
let primary = config
|
||||
.primary_cloud
|
||||
.as_deref()
|
||||
.and_then(|id| config.cloud_providers.iter().find(|entry| entry.id == id));
|
||||
|
||||
if primary.is_some_and(is_openhuman_cloud_entry) {
|
||||
if let Some(legacy) = legacy_custom_inference_provider_string(config) {
|
||||
return legacy;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(entry) = primary {
|
||||
return cloud_entry_provider_string(entry, config);
|
||||
}
|
||||
|
||||
legacy_custom_inference_provider_string(config)
|
||||
.unwrap_or_else(|| PROVIDER_OPENHUMAN.to_string())
|
||||
}
|
||||
|
||||
fn legacy_custom_inference_provider_string(config: &Config) -> Option<String> {
|
||||
let inference_url = config
|
||||
.inference_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|url| !url.is_empty())?;
|
||||
|
||||
if looks_like_openhuman_backend(inference_url) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let normalized_inference = normalize_endpoint_for_compare(inference_url);
|
||||
config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
!is_openhuman_cloud_entry(entry)
|
||||
&& normalize_endpoint_for_compare(&entry.endpoint) == normalized_inference
|
||||
})
|
||||
.map(|entry| cloud_entry_provider_string(entry, config))
|
||||
}
|
||||
|
||||
fn cloud_entry_provider_string(
|
||||
entry: &crate::openhuman::config::schema::cloud_providers::CloudProviderCreds,
|
||||
config: &Config,
|
||||
) -> String {
|
||||
if is_openhuman_cloud_entry(entry) {
|
||||
return PROVIDER_OPENHUMAN.to_string();
|
||||
}
|
||||
|
||||
let model = entry
|
||||
.default_model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|model| !model.is_empty())
|
||||
.or_else(|| {
|
||||
config
|
||||
.default_model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|model| !model.is_empty())
|
||||
})
|
||||
.unwrap_or(crate::openhuman::config::DEFAULT_MODEL);
|
||||
|
||||
format!("{}:{model}", entry.slug)
|
||||
}
|
||||
|
||||
fn is_openhuman_cloud_entry(
|
||||
entry: &crate::openhuman::config::schema::cloud_providers::CloudProviderCreds,
|
||||
) -> bool {
|
||||
entry.slug == PROVIDER_OPENHUMAN
|
||||
|| matches!(entry.auth_style, AuthStyle::OpenhumanJwt)
|
||||
|| looks_like_openhuman_backend(&entry.endpoint)
|
||||
}
|
||||
|
||||
fn normalize_endpoint_for_compare(url: &str) -> String {
|
||||
url.trim().trim_end_matches('/').to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn looks_like_openhuman_backend(url: &str) -> bool {
|
||||
let lower = url.trim().to_ascii_lowercase();
|
||||
let without_scheme = lower.split("://").nth(1).unwrap_or(&lower);
|
||||
let authority = without_scheme.split('/').next().unwrap_or("");
|
||||
let host = authority.split('@').next_back().unwrap_or(authority);
|
||||
let host_no_port = host.split(':').next().unwrap_or(host);
|
||||
matches!(
|
||||
host_no_port,
|
||||
"api.openhuman.ai" | "api.tinyhumans.ai" | "staging-api.tinyhumans.ai" | "openhuman"
|
||||
) || host_no_port.ends_with(".openhuman.ai")
|
||||
|| host_no_port.ends_with(".tinyhumans.ai")
|
||||
}
|
||||
|
||||
/// Parse a `<model>[@<temp>]` tail into `(model, override)`.
|
||||
///
|
||||
/// Tolerates whitespace around the components. Returns `temperature = None`
|
||||
|
||||
@@ -71,6 +71,16 @@ fn cloud_no_providers_falls_back_to_openhuman() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_cloud_sentinel_resolves_to_primary_custom_provider() {
|
||||
let mut config = config_with_providers(vec![oh_entry("p_oh"), openai_entry("p_oai", "openai")]);
|
||||
config.primary_cloud = Some("p_oai".to_string());
|
||||
|
||||
let (_, model) =
|
||||
create_chat_provider_from_string("reasoning", "cloud", &config).expect("build");
|
||||
assert_eq!(model, "gpt-4o");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openhuman_slug_routes_to_backend() {
|
||||
let config = config_with_providers(vec![oh_entry("p_oh")]);
|
||||
@@ -377,6 +387,71 @@ fn primary_cloud_defaults_to_openhuman_when_no_providers() {
|
||||
assert!(create_chat_provider("reasoning", &config).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloud_sentinel_resolves_to_primary_custom_provider() {
|
||||
let mut config = config_with_providers(vec![oh_entry("p_oh"), openai_entry("p_oai", "openai")]);
|
||||
config.primary_cloud = Some("p_oai".to_string());
|
||||
|
||||
assert_eq!(provider_for_role("reasoning", &config), "openai:gpt-4o");
|
||||
|
||||
let (_, model) =
|
||||
create_chat_provider("reasoning", &config).expect("primary custom provider must build");
|
||||
assert_eq!(model, "gpt-4o");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_inference_url_custom_provider_wins_over_openhuman_primary_for_unset_role() {
|
||||
let mut custom = openai_entry("p_custom", "custom");
|
||||
custom.endpoint = "https://api.example.com/v1/".to_string();
|
||||
custom.default_model = Some("gpt-4o-mini".to_string());
|
||||
|
||||
let mut config = config_with_providers(vec![oh_entry("p_oh"), custom]);
|
||||
config.primary_cloud = Some("p_oh".to_string());
|
||||
config.inference_url = Some("https://api.example.com/v1".to_string());
|
||||
|
||||
assert_eq!(
|
||||
provider_for_role("reasoning", &config),
|
||||
"custom:gpt-4o-mini"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_inference_url_without_matching_provider_stays_on_openhuman_primary() {
|
||||
let mut other = openai_entry("p_other", "other");
|
||||
other.endpoint = "https://other.example.com/v1".to_string();
|
||||
|
||||
let mut config = config_with_providers(vec![oh_entry("p_oh"), other]);
|
||||
config.primary_cloud = Some("p_oh".to_string());
|
||||
config.inference_url = Some("https://api.example.com/v1".to_string());
|
||||
|
||||
assert_eq!(provider_for_role("reasoning", &config), "openhuman");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hosted_endpoint_entry_is_treated_as_openhuman_backend() {
|
||||
let mut hosted = openai_entry("p_hosted", "custom-hosted");
|
||||
hosted.endpoint = "https://staging-api.tinyhumans.ai/openai/v1".to_string();
|
||||
hosted.auth_style = AuthStyle::Bearer;
|
||||
|
||||
let mut config = config_with_providers(vec![hosted]);
|
||||
config.primary_cloud = Some("p_hosted".to_string());
|
||||
|
||||
assert_eq!(provider_for_role("reasoning", &config), "openhuman");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_openhuman_route_ignores_legacy_inference_url() {
|
||||
let mut custom = openai_entry("p_custom", "custom");
|
||||
custom.endpoint = "https://api.example.com/v1".to_string();
|
||||
|
||||
let mut config = config_with_providers(vec![oh_entry("p_oh"), custom]);
|
||||
config.primary_cloud = Some("p_oh".to_string());
|
||||
config.inference_url = Some("https://api.example.com/v1".to_string());
|
||||
config.reasoning_provider = Some("openhuman".to_string());
|
||||
|
||||
assert_eq!(provider_for_role("reasoning", &config), "openhuman");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summarization_aliases_memory_provider() {
|
||||
let mut config = Config::default();
|
||||
|
||||
@@ -143,12 +143,41 @@ fn seed_cloud_providers(config: &mut Config, stats: &mut MigrationStats) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Default `primary_cloud` to the OpenHuman entry (the first one we just
|
||||
/// seeded, by construction). Idempotent — only sets if currently `None`.
|
||||
/// Default `primary_cloud` to the legacy custom inference entry when one was
|
||||
/// present; otherwise use the OpenHuman entry. This preserves pre-migration
|
||||
/// behavior where a configured `inference_url` handled chat instead of the
|
||||
/// hosted OpenHuman budget.
|
||||
fn set_primary_cloud(config: &mut Config, stats: &mut MigrationStats) {
|
||||
if config.primary_cloud.is_some() {
|
||||
return;
|
||||
}
|
||||
let legacy_custom = config
|
||||
.inference_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|url| !url.is_empty() && !looks_like_openhuman(url))
|
||||
.and_then(|url| {
|
||||
let normalized = url.trim_end_matches('/').to_ascii_lowercase();
|
||||
config.cloud_providers.iter().find(|entry| {
|
||||
entry.slug != "openhuman"
|
||||
&& entry
|
||||
.endpoint
|
||||
.trim()
|
||||
.trim_end_matches('/')
|
||||
.to_ascii_lowercase()
|
||||
== normalized
|
||||
})
|
||||
});
|
||||
if let Some(entry) = legacy_custom {
|
||||
config.primary_cloud = Some(entry.id.clone());
|
||||
stats.primary_cloud_set = true;
|
||||
log::debug!(
|
||||
"[migrations][unify-ai] primary_cloud set to legacy custom entry id={}",
|
||||
entry.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let oh = config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
@@ -248,6 +277,9 @@ fn looks_like_openhuman(url: &str) -> bool {
|
||||
let host_no_port = host.split(':').next().unwrap_or(host);
|
||||
host_no_port == "api.openhuman.ai"
|
||||
|| host_no_port.ends_with(".openhuman.ai")
|
||||
|| host_no_port == "api.tinyhumans.ai"
|
||||
|| host_no_port == "staging-api.tinyhumans.ai"
|
||||
|| host_no_port.ends_with(".tinyhumans.ai")
|
||||
// Allow bare "openhuman" for local/dev names (e.g. Docker compose service names).
|
||||
|| host_no_port == "openhuman"
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@ fn legacy_inference_url_becomes_custom_entry() {
|
||||
.expect("custom entry must be seeded");
|
||||
assert_eq!(custom.endpoint, "https://api.example.com/v1");
|
||||
assert_eq!(custom.default_model.as_deref(), Some("gpt-4o"));
|
||||
assert_eq!(
|
||||
c.primary_cloud.as_deref(),
|
||||
Some(custom.id.as_str()),
|
||||
"legacy custom inference must remain the default cloud target after migration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user