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
2 changes: 1 addition & 1 deletion .github/tauri-cef-expected-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e22ec719034fdac3994c42a3c040fafa10672219
c90c8a330056286e7c0d05439ae3d4527fa4fafe
2 changes: 1 addition & 1 deletion app/src-tauri/vendor/tauri-cef
44 changes: 43 additions & 1 deletion src/openhuman/inference/provider/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ pub const PROVIDER_OPENHUMAN: &str = "openhuman";
/// Prefix for Ollama-local providers: `"ollama:<model>"`.
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:<slug>"`. Lookups also try the bare `<slug>`
Expand Down Expand Up @@ -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,
Expand Down
42 changes: 42 additions & 0 deletions src/openhuman/inference/provider/factory_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Provider>` 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();
Expand Down
Loading