diff --git a/.github/tauri-cef-expected-sha b/.github/tauri-cef-expected-sha index 1b3addaee1..cab89c30c7 100644 --- a/.github/tauri-cef-expected-sha +++ b/.github/tauri-cef-expected-sha @@ -1 +1 @@ -e22ec719034fdac3994c42a3c040fafa10672219 +c90c8a330056286e7c0d05439ae3d4527fa4fafe \ No newline at end of file diff --git a/app/src-tauri/vendor/tauri-cef b/app/src-tauri/vendor/tauri-cef index 4cabccfa82..c90c8a3300 160000 --- a/app/src-tauri/vendor/tauri-cef +++ b/app/src-tauri/vendor/tauri-cef @@ -1 +1 @@ -Subproject commit 4cabccfa82c53e5f9d8409894cc47a648057c90e +Subproject commit c90c8a330056286e7c0d05439ae3d4527fa4fafe diff --git a/src/openhuman/inference/provider/factory.rs b/src/openhuman/inference/provider/factory.rs index ca8d6d6e76..421ae61650 100644 --- a/src/openhuman/inference/provider/factory.rs +++ b/src/openhuman/inference/provider/factory.rs @@ -36,6 +36,21 @@ pub const PROVIDER_OPENHUMAN: &str = "openhuman"; /// Prefix for Ollama-local providers: `"ollama:"`. pub const OLLAMA_PROVIDER_PREFIX: &str = "ollama:"; +fn is_abstract_tier_model(model: &str) -> bool { + use crate::openhuman::config::{ + MODEL_AGENTIC_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, + }; + // No dedicated constant for the summarization tier yet; keep the literal + // in sync with the tier name used by the summarizer sub-agent. + const MODEL_SUMMARIZATION_V1: &str = "summarization-v1"; + let trimmed = model.trim(); + trimmed == MODEL_REASONING_V1 + || trimmed == MODEL_REASONING_QUICK_V1 + || trimmed == MODEL_AGENTIC_V1 + || trimmed == MODEL_CODING_V1 + || trimmed == MODEL_SUMMARIZATION_V1 +} + /// Auth-profile storage key for a slug-keyed provider. /// /// New writes use `"provider:"`. Lookups also try the bare `` @@ -426,12 +441,39 @@ fn make_cloud_provider_by_slug( // Resolve effective model: use provided model if non-empty, else fall back // to the entry's legacy default_model (if any), else empty → error. - let effective_model = if model.trim().is_empty() { + let mut effective_model = if model.trim().is_empty() { entry.default_model.clone().unwrap_or_default() } else { model.to_string() }; + if entry.auth_style != AuthStyle::OpenhumanJwt && is_abstract_tier_model(&effective_model) { + if let Some(default_model) = entry + .default_model + .as_deref() + .map(str::trim) + .filter(|m| !m.is_empty() && !is_abstract_tier_model(m)) + { + log::info!( + "[providers][chat-factory] role={} slug={} remapping abstract model {} -> {}", + role, + slug, + effective_model, + default_model + ); + effective_model = default_model.to_string(); + } else { + anyhow::bail!( + "[chat-factory] model '{}' is an abstract tier for role '{}', \ + but cloud provider slug '{}' has no concrete default_model configured. \ + Set cloud_providers[].default_model to a provider-native model id (e.g. deepseek-v4-pro).", + effective_model, + role, + slug + ); + } + } + log::info!( "[providers][chat-factory] role={} slug={} model={} endpoint_host={}", role, diff --git a/src/openhuman/inference/provider/factory_test.rs b/src/openhuman/inference/provider/factory_test.rs index b82c47741c..7db20e557a 100644 --- a/src/openhuman/inference/provider/factory_test.rs +++ b/src/openhuman/inference/provider/factory_test.rs @@ -124,6 +124,48 @@ fn openrouter_slug_model() { assert_eq!(model, "meta-llama/llama-3.1-8b"); } +#[test] +fn custom_provider_remaps_abstract_tier_to_concrete_default_model() { + let mut config = Config::default(); + config.cloud_providers.push(CloudProviderCreds { + id: "p_ds".to_string(), + slug: "deepseek".to_string(), + label: "DeepSeek".to_string(), + endpoint: "https://api.deepseek.com/v1".to_string(), + auth_style: AuthStyle::Bearer, + default_model: Some("deepseek-v4-pro".to_string()), + ..Default::default() + }); + + let (_, model) = + create_chat_provider_from_string("reasoning", "deepseek:reasoning-v1", &config) + .expect("abstract tier should remap to concrete default model"); + assert_eq!(model, "deepseek-v4-pro"); +} + +#[test] +fn custom_provider_rejects_abstract_tier_without_concrete_default_model() { + let mut config = Config::default(); + config.cloud_providers.push(CloudProviderCreds { + id: "p_ds".to_string(), + slug: "deepseek".to_string(), + label: "DeepSeek".to_string(), + endpoint: "https://api.deepseek.com/v1".to_string(), + auth_style: AuthStyle::Bearer, + default_model: None, + ..Default::default() + }); + + // Can't use `.expect_err(..)` here because `Box` doesn't + // implement `Debug`, so the success arm has no Debug to print. + let err = match create_chat_provider_from_string("reasoning", "deepseek:reasoning-v1", &config) + { + Ok(_) => panic!("abstract tier without concrete provider default should fail"), + Err(e) => e, + }; + assert!(err.to_string().contains("abstract tier")); +} + #[test] fn orcarouter_slug_model() { let mut config = Config::default();