fix(inference): synth local-runtime entry for list_models when no cloud_providers row (Sentry TAURI-RUST-28Z)

This commit is contained in:
CodeGhost21
2026-05-29 04:21:15 +05:30
committed by GitHub
parent 0bd17ea5e4
commit 97d83a1f77
2 changed files with 217 additions and 1 deletions
@@ -106,7 +106,12 @@ impl Default for CloudProviderCreds {
/// routing is unaffected — the `ollama:<model>` prefix branch in
/// `factory::create_chat_provider_from_string` fires before the
/// `<slug>:<model>` cloud-provider lookup, so a synthetic `ollama` entry
/// never reaches `make_cloud_provider_by_slug`.
/// never reaches `make_cloud_provider_by_slug`. When no `cloud_providers`
/// row exists (config drift, upgrade from a build that only persisted
/// `config.local_ai.base_url`, flush-vs-probe race),
/// [`crate::openhuman::inference::provider::ops::list_configured_models`]
/// falls back to a synthetic entry via `synthesize_local_runtime_entry`
/// (Sentry TAURI-RUST-28Z fix). The same fallback applies to `lmstudio`.
pub fn is_slug_reserved(s: &str) -> bool {
matches!(s.trim(), "" | "cloud" | "openhuman" | "pid")
}
+211
View File
@@ -46,11 +46,15 @@ async fn list_configured_models_from_config(
log::debug!("[providers][list_models] provider_id={}", provider_id);
// Explicit `cloud_providers` entry wins (e.g. a user-pointed remote
// ollama box at https://ollama.example.com/v1). Falling back to the
// local-runtime synthesis below only happens when no entry matches.
let entry = config
.cloud_providers
.iter()
.find(|e| e.id == provider_id || e.slug == provider_id)
.cloned()
.or_else(|| synthesize_local_runtime_entry(&provider_id, config))
.ok_or_else(|| format!("no cloud provider with id or slug '{}' found", provider_id))?;
let base = entry.endpoint.trim_end_matches('/');
@@ -274,6 +278,60 @@ fn json_value_kind(v: &serde_json::Value) -> &'static str {
}
}
/// Synthesize a transient [`CloudProviderCreds`] entry for the well-known
/// local-runtime slugs (`ollama`, `lmstudio`) so [`list_configured_models`]
/// can probe their OpenAI-compatible `/v1/models` endpoint even when the
/// user has not registered a matching `cloud_providers` row.
///
/// Background: the AI settings panel registers an `ollama` `cloud_providers`
/// entry when the user configures Ollama (see comment on
/// [`crate::openhuman::config::schema::cloud_providers::is_slug_reserved`]),
/// but in practice some users hit
/// `inference_list_models("ollama")` without that entry — config drift,
/// flush-vs-probe race, or upgrade from a build that only persisted
/// `config.local_ai.base_url`. Sentry TAURI-RUST-28Z captures this:
/// 24 events / 7d, all `domain=rpc, method=openhuman.inference_list_models,
/// operation=invoke_method`. Without this fallback, the dropdown surfaces
/// the bare `"no cloud provider with id or slug 'ollama' found"` error
/// (also visible in the Sentry breadcrumb) instead of returning models.
///
/// Returns `None` for any slug that is not a recognized local-runtime
/// alias — callers continue down the normal "no cloud provider" error
/// path for `openai` / `anthropic` / opaque ids / typos.
fn synthesize_local_runtime_entry(
slug: &str,
config: &crate::openhuman::config::Config,
) -> Option<crate::openhuman::config::schema::cloud_providers::CloudProviderCreds> {
use crate::openhuman::config::schema::cloud_providers::{AuthStyle, CloudProviderCreds};
let endpoint = match slug {
// Ollama's OpenAI-compatible surface at `<base>/v1/models` returns
// the same `{"data": [...]}` shape the existing parser handles, so
// we route through that rather than the native `/api/tags`.
"ollama" => {
let base = crate::openhuman::inference::local::ollama_base_url_from_config(config);
format!("{}/v1", base.trim_end_matches('/'))
}
// `lm_studio_base_url` already ends in `/v1`.
"lmstudio" => crate::openhuman::inference::local::lm_studio::lm_studio_base_url(config),
_ => return None,
};
Some(CloudProviderCreds {
id: format!("synthetic_local_{slug}"),
slug: slug.to_string(),
label: slug.to_string(),
endpoint,
// Local runtimes accept unauthenticated requests on loopback.
// The probe at `<endpoint>/models` runs without an Authorization
// header — `lookup_key_for_slug` may still return a key, but
// `AuthStyle::None` ignores it (see auth-style match below).
auth_style: AuthStyle::None,
legacy_type: None,
default_model: None,
})
}
fn is_openrouter_provider(
entry: &crate::openhuman::config::schema::cloud_providers::CloudProviderCreds,
) -> bool {
@@ -1958,6 +2016,63 @@ mod tests {
}
}
// ── synthesize_local_runtime_entry (TAURI-RUST-28Z fallback) ────────────
#[test]
fn synthesize_local_runtime_entry_ollama_returns_v1_endpoint_with_no_auth() {
// Sentry TAURI-RUST-28Z fires when `inference_list_models("ollama")`
// runs against a config that has no `ollama` cloud_providers entry.
// The synth fallback must produce an entry routed to Ollama's
// OpenAI-compatible `/v1/models` surface at the resolved base URL,
// with `AuthStyle::None` so the probe runs without an Authorization
// header (loopback Ollama accepts unauthenticated requests).
let config = Config::default();
let entry = synthesize_local_runtime_entry("ollama", &config)
.expect("ollama must produce a synthetic entry");
assert_eq!(entry.slug, "ollama");
assert_eq!(entry.auth_style, AuthStyle::None);
assert!(
entry.endpoint.ends_with("/v1"),
"ollama endpoint must terminate at /v1 so `<endpoint>/models` hits the OpenAI-compat surface; got {}",
entry.endpoint
);
}
#[test]
fn synthesize_local_runtime_entry_lmstudio_returns_v1_endpoint_with_no_auth() {
// LM Studio's default `lm_studio_base_url` already terminates at
// `/v1`; the synth must preserve that and select `AuthStyle::None`
// so the probe doesn't attach a bearer header (LM Studio runs
// unauthenticated on loopback).
let config = Config::default();
let entry = synthesize_local_runtime_entry("lmstudio", &config)
.expect("lmstudio must produce a synthetic entry");
assert_eq!(entry.slug, "lmstudio");
assert_eq!(entry.auth_style, AuthStyle::None);
assert!(
entry.endpoint.ends_with("/v1"),
"lmstudio endpoint must terminate at /v1; got {}",
entry.endpoint
);
}
#[test]
fn synthesize_local_runtime_entry_returns_none_for_unknown_slug() {
// Only `ollama` and `lmstudio` are the recognized local-runtime
// aliases. Every other slug — built-in cloud providers (`openai`,
// `anthropic`), opaque ids (`p_random_xyz`), or typos — must fall
// through to the existing "no cloud provider" error. Pinning this
// rejection contract guards against the synth growing into a
// blanket "any unknown slug points at localhost" matcher.
let config = Config::default();
for slug in ["openai", "anthropic", "openrouter", "p_random_xyz", "", " "] {
assert!(
synthesize_local_runtime_entry(slug, &config).is_none(),
"{slug:?} must NOT synthesize a local-runtime entry"
);
}
}
#[test]
fn parse_models_response_handles_non_object_body() {
// Provider returned a bare array / string / number at the
@@ -2160,4 +2275,100 @@ mod tests {
"raw secret must not survive into the SessionExpired reason: {reason}"
);
}
#[test]
fn synthesize_local_runtime_entry_ollama_respects_config_base_url() {
// The synth must honor `config.local_ai.base_url` (the same
// priority `ollama_base_url_from_config` uses for chat routing).
// This is the path users hit when they point Ollama at a non-loopback
// host (e.g. a LAN box at 192.168.1.5).
let mut config = Config::default();
config.local_ai.base_url = Some("http://192.168.1.5:11434".to_string());
let entry = synthesize_local_runtime_entry("ollama", &config)
.expect("ollama with custom base_url must still synthesize");
assert_eq!(
entry.endpoint, "http://192.168.1.5:11434/v1",
"synth must use config.local_ai.base_url and append /v1 once",
);
}
#[test]
fn cloud_providers_entry_takes_precedence_over_local_runtime_synthesis() {
// Pin the precedence: if the user has explicitly added an `ollama`
// entry to `cloud_providers` (e.g. a remote ollama box at
// https://ollama.example.com/v1), that entry MUST win — the synth
// fallback is reached only when the find returns `None`. Mirrors
// the lookup in `list_configured_models_from_config` so a future
// refactor that swaps `find().or_else(synth)` for unconditional
// synthesis fails this test loudly.
let mut config = Config::default();
config.cloud_providers.push(CloudProviderCreds {
id: "p_ollama_explicit".to_string(),
slug: "ollama".to_string(),
label: "Remote Ollama".to_string(),
endpoint: "https://ollama.example.com/v1".to_string(),
auth_style: AuthStyle::Bearer,
legacy_type: None,
default_model: None,
});
let resolved = config
.cloud_providers
.iter()
.find(|e| e.id == "ollama" || e.slug == "ollama")
.cloned()
.or_else(|| synthesize_local_runtime_entry("ollama", &config))
.expect("either explicit or synth must resolve");
assert_eq!(
resolved.endpoint, "https://ollama.example.com/v1",
"explicit cloud_providers entry must beat local-runtime synth",
);
assert_eq!(resolved.auth_style, AuthStyle::Bearer);
}
#[test]
fn missing_cloud_providers_entry_falls_back_to_local_runtime_synth() {
// The TAURI-RUST-28Z regression contract: when no `ollama` entry
// exists in `cloud_providers` AND the slug is a recognized
// local-runtime alias, the find/synth chain must yield a synthetic
// entry (instead of `None`, which produces the
// "no cloud provider with id or slug 'ollama' found" Sentry error).
let config = Config::default();
assert!(
config.cloud_providers.is_empty(),
"precondition: clean config has no providers configured",
);
let resolved = config
.cloud_providers
.iter()
.find(|e| e.id == "ollama" || e.slug == "ollama")
.cloned()
.or_else(|| synthesize_local_runtime_entry("ollama", &config));
assert!(
resolved.is_some(),
"ollama must resolve via synth when cloud_providers is empty"
);
assert_eq!(resolved.unwrap().slug, "ollama");
}
#[test]
fn missing_cloud_providers_entry_for_unknown_slug_still_errors() {
// The synth is intentionally narrow: only `ollama` and `lmstudio`
// get fallback routing. An unknown slug with no `cloud_providers`
// match must continue to produce `None` (which the caller surfaces
// as the "no cloud provider" error) — otherwise typos would
// silently route to localhost.
let config = Config::default();
let resolved = config
.cloud_providers
.iter()
.find(|e| e.id == "tpyo" || e.slug == "tpyo")
.cloned()
.or_else(|| synthesize_local_runtime_entry("tpyo", &config));
assert!(
resolved.is_none(),
"unknown slug with no cloud_providers entry must NOT synthesize",
);
}
}