diff --git a/src/openhuman/inference/local/lm_studio.rs b/src/openhuman/inference/local/lm_studio.rs index cf5317402..e24e6aa8d 100644 --- a/src/openhuman/inference/local/lm_studio.rs +++ b/src/openhuman/inference/local/lm_studio.rs @@ -291,11 +291,28 @@ pub(crate) struct LmStudioNativeModelsResponse { /// Map a normalized `…/v1` base URL to the LM Studio native models endpoint /// `…/api/v0/models` (a sibling of `/v1`, served at the host root). pub(crate) fn lm_studio_native_models_url(v1_base_url: &str) -> String { - let root = v1_base_url + format!("{}/api/v0/models", host_root_of(v1_base_url)) +} + +/// Strip a trailing `/v1` so sibling endpoints served at the host root can be +/// derived from an OpenAI-compatible base URL. +pub(crate) fn host_root_of(v1_base_url: &str) -> &str { + v1_base_url .trim_end_matches('/') .trim_end_matches("/v1") - .trim_end_matches('/'); - format!("{root}/api/v0/models") + .trim_end_matches('/') +} + +/// Ollama-native `GET /api/tags` URL derived from an OpenAI-compatible base. +/// +/// Only used as the one-shot 404 fallback in +/// [`LocalAiService::list_lm_studio_models`](crate::openhuman::inference::local::service::LocalAiService): +/// some runtimes are reachable on an OpenAI-shaped base URL but expose only the +/// Ollama listing (e.g. plain Ollama configured with a `/v1` base). Discovery is +/// still chosen by provider type first — this is a recovery path, not a probe +/// order (GH #5055). +pub(crate) fn ollama_tags_fallback_url(v1_base_url: &str) -> String { + format!("{}/api/tags", host_root_of(v1_base_url)) } /// Resolve the context window LM Studio reports for `model_id` from a native @@ -336,6 +353,37 @@ mod tests { ); } + /// GH #5055: the `/api/tags` fallback URL is a sibling of `/v1` at the host + /// root. Appending to the `/v1` base would produce `/v1/api/tags` — the + /// exact malformed request LM Studio logs as `Unexpected endpoint or + /// method` (GH #5053). + #[test] + fn ollama_tags_fallback_url_is_host_rooted_not_v1_suffixed() { + assert_eq!( + ollama_tags_fallback_url("http://localhost:1234/v1"), + "http://localhost:1234/api/tags" + ); + assert_eq!( + ollama_tags_fallback_url("http://127.0.0.1:1234/v1/"), + "http://127.0.0.1:1234/api/tags" + ); + assert_eq!( + ollama_tags_fallback_url("https://lm.example.com/lmstudio/v1"), + "https://lm.example.com/lmstudio/api/tags" + ); + // A host-rooted base (no /v1) is left alone. + assert_eq!( + ollama_tags_fallback_url("http://localhost:11434"), + "http://localhost:11434/api/tags" + ); + for url in [ + ollama_tags_fallback_url("http://localhost:1234/v1"), + ollama_tags_fallback_url("http://localhost:1234/v1/"), + ] { + assert!(!url.contains("/v1/api/tags"), "malformed probe URL: {url}"); + } + } + #[test] fn context_window_prefers_loaded_then_max() { let resp: LmStudioNativeModelsResponse = serde_json::from_str( diff --git a/src/openhuman/inference/local/ollama.rs b/src/openhuman/inference/local/ollama.rs index 486c48e82..74252033d 100644 --- a/src/openhuman/inference/local/ollama.rs +++ b/src/openhuman/inference/local/ollama.rs @@ -312,6 +312,14 @@ impl OllamaPullProgress { pub(crate) struct OllamaTagsResponse { #[serde(default)] pub models: Vec, + /// Set when the server answered with an error envelope rather than a + /// catalog. LM Studio replies to unknown paths with `200 {"error": …}` and + /// no `models` (GH #5053), which is indistinguishable from a real catalog + /// by `models` alone — a fresh Ollama with nothing pulled legitimately + /// returns `{"models":[]}`. Callers must branch on this field, not on + /// emptiness. + #[serde(default)] + pub error: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/src/openhuman/inference/local/service/lm_studio.rs b/src/openhuman/inference/local/service/lm_studio.rs index 38163e344..b1f49ecfc 100644 --- a/src/openhuman/inference/local/service/lm_studio.rs +++ b/src/openhuman/inference/local/service/lm_studio.rs @@ -1,9 +1,10 @@ use crate::openhuman::config::Config; use crate::openhuman::inference::local::lm_studio::{ - apply_lm_studio_auth, lm_studio_base_url, LmStudioChatCompletionRequest, - LmStudioChatCompletionResponse, LmStudioChatMessage, LmStudioModelsResponse, + apply_lm_studio_auth, lm_studio_base_url, ollama_tags_fallback_url, + LmStudioChatCompletionRequest, LmStudioChatCompletionResponse, LmStudioChatMessage, + LmStudioModelsResponse, }; -use crate::openhuman::inference::local::ollama::OllamaModelTag; +use crate::openhuman::inference::local::ollama::{OllamaModelTag, OllamaTagsResponse}; use crate::openhuman::inference::model_ids; use super::LocalAiService; @@ -43,11 +44,15 @@ impl LocalAiService { ) -> Result, String> { let base = lm_studio_base_url(config); let url = format!("{base}/models"); + // GH #5055: log the *resolved* discovery URL so a wrong base URL is + // diagnosable from app logs alone, without reproducing against the + // runtime's own request log. tracing::debug!( target: "local_ai::lm_studio", %base, - %url, - "[local_ai:lm_studio] list_models: sending GET" + discovery_url = %url, + api = "openai_v1_models", + "[local_ai:lm_studio] list_models: resolved discovery URL — sending GET" ); let request = self @@ -78,6 +83,20 @@ impl LocalAiService { body = %diagnostic_body_snippet(&body), "[local_ai:lm_studio] list_models: non-success response" ); + + // GH #5055: a 404 on `/v1/models` means this host is reachable but + // does not serve the OpenAI catalog. Try the Ollama-native + // `/api/tags` exactly once before giving up, so a runtime that only + // speaks the Ollama listing still discovers its models. Discovery is + // still selected by provider *type* first (`model_discovery_api`); + // this is a recovery path, never a probe order, and it never runs + // for any status other than 404. + if status == reqwest::StatusCode::NOT_FOUND { + if let Some(models) = self.list_ollama_tags_fallback(config, &base).await { + return Ok(models); + } + } + return Err(format!( "lm studio models failed with status {}{}", status, @@ -120,6 +139,122 @@ impl LocalAiService { .collect()) } + /// One-shot Ollama-native `/api/tags` fallback for a `/v1/models` 404. + /// + /// Returns `Some(models)` only when the fallback actually produced a + /// catalog; every failure returns `None` so the caller surfaces the original + /// `/v1/models` error rather than a confusing second one. Logs at WARN on + /// entry because taking this path means the configured base URL and the + /// provider type disagree — the user should fix the configuration even + /// though discovery recovered (GH #5055). + async fn list_ollama_tags_fallback( + &self, + config: &Config, + base: &str, + ) -> Option> { + let fallback_url = ollama_tags_fallback_url(base); + tracing::warn!( + target: "local_ai::lm_studio", + %base, + discovery_url = %fallback_url, + api = "ollama_api_tags", + "[local_ai:lm_studio] list_models: /v1/models returned 404 — retrying once against \ + the Ollama-native /api/tags. Check the configured base URL: an OpenAI-compatible \ + runtime should serve /v1/models." + ); + + let request = self + .http + .get(&fallback_url) + .timeout(std::time::Duration::from_secs(5)); + let response = match apply_lm_studio_auth(request, config).send().await { + Ok(r) => r, + Err(e) => { + tracing::debug!( + target: "local_ai::lm_studio", + url = %fallback_url, + error = %e, + "[local_ai:lm_studio] /api/tags fallback request failed" + ); + return None; + } + }; + + let status = response.status(); + if !status.is_success() { + tracing::debug!( + target: "local_ai::lm_studio", + url = %fallback_url, + %status, + "[local_ai:lm_studio] /api/tags fallback returned non-success" + ); + return None; + } + + let body = match response.text().await { + Ok(b) => b, + Err(e) => { + tracing::debug!( + target: "local_ai::lm_studio", + url = %fallback_url, + error = %e, + "[local_ai:lm_studio] /api/tags fallback body read failed" + ); + return None; + } + }; + let payload: OllamaTagsResponse = match serde_json::from_str(&body) { + Ok(p) => p, + Err(e) => { + tracing::debug!( + target: "local_ai::lm_studio", + url = %fallback_url, + error = %e, + body = %diagnostic_body_snippet(&body), + "[local_ai:lm_studio] /api/tags fallback parse failed" + ); + return None; + } + }; + + // Reject on an explicit error envelope, NOT on emptiness. LM Studio + // answers unknown paths with `200 {"error": …}` and no models + // (GH #5053) — that is the case that must fall through to the original + // /v1/models error. A fresh Ollama with nothing pulled yet legitimately + // returns `{"models":[]}`, and treating that as a failure hid a + // reachable runtime behind a 404, so the UI could not offer the + // model-download action. + if let Some(error) = payload + .error + .as_deref() + .map(str::trim) + .filter(|e| !e.is_empty()) + { + tracing::debug!( + target: "local_ai::lm_studio", + url = %fallback_url, + error = %error, + "[local_ai:lm_studio] /api/tags fallback returned an error envelope — not a recovery" + ); + return None; + } + if payload.models.is_empty() { + tracing::info!( + target: "local_ai::lm_studio", + url = %fallback_url, + "[local_ai:lm_studio] /api/tags fallback reached a reachable runtime with an empty catalog — recovering as zero models" + ); + } + + tracing::info!( + target: "local_ai::lm_studio", + url = %fallback_url, + model_count = payload.models.len(), + "[local_ai:lm_studio] recovered model discovery via the Ollama /api/tags fallback" + ); + Some(payload.models) + } + pub(in crate::openhuman::inference::local::service) async fn has_lm_studio_model( &self, config: &Config, diff --git a/src/openhuman/inference/local/service/ollama_admin_tests.rs b/src/openhuman/inference/local/service/ollama_admin_tests.rs index fb0ff1078..0cfd8a786 100644 --- a/src/openhuman/inference/local/service/ollama_admin_tests.rs +++ b/src/openhuman/inference/local/service/ollama_admin_tests.rs @@ -1277,3 +1277,187 @@ async fn diagnostics_gates_models_by_context_window() { std::env::remove_var("OPENHUMAN_OLLAMA_BASE_URL"); } } + +// ── GH #5055: one-shot /api/tags fallback on a /v1/models 404 ─────────────── +// +// Discovery is still chosen by provider *type* (`model_discovery_api`); these +// cover the recovery path taken when the chosen OpenAI-compatible endpoint +// answers 404, so a runtime that only speaks the Ollama listing is not left +// with an empty catalog. + +/// A `/v1/models` 404 falls back to the host-rooted `/api/tags` exactly once +/// and returns that catalog. +#[tokio::test] +async fn v1_models_404_falls_back_to_ollama_api_tags() { + let _guard = crate::openhuman::inference::inference_test_guard(); + + let app = Router::new() + .route( + "/v1/models", + get(|| async { (axum::http::StatusCode::NOT_FOUND, "404 page not found") }), + ) + .route( + "/api/tags", + get(|| async { + Json(json!({ + "models": [ + { "name": "local-model", "size": 42, "modified_at": "2026-01-01T00:00:00Z" } + ] + })) + }), + ); + let base = spawn_mock(app).await; + let config = lm_studio_config(&base); + let service = LocalAiService::new(&config); + + let models = service + .list_lm_studio_models(&config) + .await + .expect("the /api/tags fallback should recover discovery"); + + assert_eq!(models.len(), 1); + assert_eq!(models[0].name, "local-model"); +} + +/// When neither endpoint serves a catalog, the caller sees the original +/// `/v1/models` failure — not a second, more confusing error from the fallback. +#[tokio::test] +async fn v1_models_404_reports_the_original_error_when_fallback_also_fails() { + let _guard = crate::openhuman::inference::inference_test_guard(); + + // The fallback returns a DISTINCT status (503, not 404). Without that, + // Axum's implicit fallback route also answers 404 and the assertion below + // would pass even if the implementation surfaced the /api/tags failure. + let app = Router::new() + .route( + "/v1/models", + get(|| async { + ( + axum::http::StatusCode::NOT_FOUND, + "404 page not found: /v1/models", + ) + }), + ) + .route( + "/api/tags", + get(|| async { + ( + axum::http::StatusCode::SERVICE_UNAVAILABLE, + "503 fallback unavailable", + ) + }), + ); + let base = spawn_mock(app).await; + let config = lm_studio_config(&base); + let service = LocalAiService::new(&config); + + let err = service + .list_lm_studio_models(&config) + .await + .expect_err("no catalog anywhere must fail"); + + assert!( + err.contains("404"), + "expected the original /v1/models status, got: {err}" + ); + assert!( + !err.contains("503"), + "the fallback failure must not replace the original error, got: {err}" + ); +} + +/// LM Studio answers unknown paths with `200 {"error": …}` and no models +/// (GH #5053). That ERROR ENVELOPE must not be treated as a recovery, +/// otherwise discovery silently "succeeds" with zero models. +#[tokio::test] +async fn error_envelope_api_tags_fallback_is_not_treated_as_recovery() { + let _guard = crate::openhuman::inference::inference_test_guard(); + + let app = Router::new() + .route( + "/v1/models", + get(|| async { (axum::http::StatusCode::NOT_FOUND, "404 page not found") }), + ) + .route( + "/api/tags", + get(|| async { Json(json!({ "error": "Unexpected endpoint or method." })) }), + ); + let base = spawn_mock(app).await; + let config = lm_studio_config(&base); + let service = LocalAiService::new(&config); + + let err = service + .list_lm_studio_models(&config) + .await + .expect_err("an error envelope is not a recovery"); + + assert!(err.contains("404"), "got: {err}"); +} + +/// A fresh Ollama with nothing pulled answers `{"models":[]}`. That is a +/// REACHABLE runtime, not a failure: rejecting it hid the server behind the +/// original 404 so the UI could not offer the model-download action. +#[tokio::test] +async fn empty_api_tags_fallback_recovers_as_zero_models() { + let _guard = crate::openhuman::inference::inference_test_guard(); + + let app = Router::new() + .route( + "/v1/models", + get(|| async { (axum::http::StatusCode::NOT_FOUND, "404 page not found") }), + ) + .route("/api/tags", get(|| async { Json(json!({ "models": [] })) })); + let base = spawn_mock(app).await; + let config = lm_studio_config(&base); + let service = LocalAiService::new(&config); + + let models = service + .list_lm_studio_models(&config) + .await + .expect("a reachable runtime with no models is not an error"); + + assert!( + models.is_empty(), + "expected an empty catalog, got {models:?}" + ); +} + +/// Any non-404 failure must NOT trigger the fallback: a 500 is a server fault, +/// not a wrong-endpoint signal, and probing a second path would mask it. +#[tokio::test] +async fn non_404_status_does_not_trigger_the_fallback() { + let _guard = crate::openhuman::inference::inference_test_guard(); + + let hits = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let hits_route = std::sync::Arc::clone(&hits); + let app = Router::new() + .route( + "/v1/models", + get(|| async { (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "boom") }), + ) + .route( + "/api/tags", + get(move || { + let hits = std::sync::Arc::clone(&hits_route); + async move { + hits.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Json(json!({ "models": [{ "name": "should-not-be-used" }] })) + } + }), + ); + let base = spawn_mock(app).await; + let config = lm_studio_config(&base); + let service = LocalAiService::new(&config); + + let err = service + .list_lm_studio_models(&config) + .await + .expect_err("a 500 must surface, not fall back"); + + assert!(err.contains("500"), "got: {err}"); + assert_eq!( + hits.load(std::sync::atomic::Ordering::SeqCst), + 0, + "the /api/tags fallback must not run for a non-404 status" + ); +} diff --git a/src/openhuman/inference/model_ids.rs b/src/openhuman/inference/model_ids.rs index f5c03e346..bc88534d5 100644 --- a/src/openhuman/inference/model_ids.rs +++ b/src/openhuman/inference/model_ids.rs @@ -19,7 +19,14 @@ pub(crate) const DEFAULT_OLLAMA_EMBED_MODEL: &str = "bge-m3"; /// Chat models allowed in the current local Ollama build. /// Any resolved chat model ID not listed here is redirected to `MVP_DEFAULT_CHAT_MODEL`. -const MVP_ALLOWED_CHAT_MODELS: &[&str] = &["gemma3:1b-it-qat", "gemma4:e4b-it-q8_0"]; +/// +/// Every id here must be pullable from the public Ollama library as written — +/// an entry that does not resolve makes the allowlist silently redirect the +/// user back to the default, or leaves them with a model that `ollama pull` +/// cannot fetch (GH #5055). `gemma4:*` was such an entry: there is no `gemma4` +/// namespace on Ollama. The intended model is Gemma 3n, published as +/// `gemma3n:e4b-it-q8_0`. +const MVP_ALLOWED_CHAT_MODELS: &[&str] = &["gemma3:1b-it-qat", "gemma3n:e4b-it-q8_0"]; const MVP_DEFAULT_CHAT_MODEL: &str = "gemma3:1b-it-qat"; /// Vision models allowed in MVP — only disabled (empty string) since the @@ -233,10 +240,10 @@ mod tests { } #[test] - fn chat_model_allows_requested_ollama_gemma4_q8() { + fn chat_model_allows_requested_ollama_gemma3n_q8() { let mut config = test_config(); - config.local_ai.chat_model_id = "gemma4:e4b-it-q8_0".to_string(); - assert_eq!(effective_chat_model_id(&config), "gemma4:e4b-it-q8_0"); + config.local_ai.chat_model_id = "gemma3n:e4b-it-q8_0".to_string(); + assert_eq!(effective_chat_model_id(&config), "gemma3n:e4b-it-q8_0"); } #[test] @@ -272,8 +279,30 @@ mod tests { config.local_ai.chat_model_id = "gemma3:270m-it-qat".to_string(); assert_eq!(effective_chat_model_id(&config), MVP_DEFAULT_CHAT_MODEL); - config.local_ai.chat_model_id = "gemma4:e4b".to_string(); + // Bare `gemma3n:e4b` is a real Ollama tag but is NOT the allowlisted + // quantization, so it still redirects to the default. + config.local_ai.chat_model_id = "gemma3n:e4b".to_string(); assert_eq!(effective_chat_model_id(&config), MVP_DEFAULT_CHAT_MODEL); + + // The retired `gemma4:*` namespace does not exist on Ollama at all. + config.local_ai.chat_model_id = "gemma4:e4b-it-q8_0".to_string(); + assert_eq!(effective_chat_model_id(&config), MVP_DEFAULT_CHAT_MODEL); + } + + /// GH #5055: every allowlisted chat model must be a real, pullable Ollama + /// id. `gemma4:*` shipped for a while and could never be pulled. + #[test] + fn mvp_chat_allowlist_has_no_unpullable_namespaces() { + for model in MVP_ALLOWED_CHAT_MODELS { + assert!( + !model.starts_with("gemma4:"), + "`{model}` is not a real Ollama model — there is no gemma4 namespace" + ); + assert!( + model.contains(':'), + "`{model}` must be a fully-qualified `:` id" + ); + } } #[test] diff --git a/src/openhuman/inference/presets.rs b/src/openhuman/inference/presets.rs index 0981ec6a5..a322a70d8 100644 --- a/src/openhuman/inference/presets.rs +++ b/src/openhuman/inference/presets.rs @@ -169,11 +169,19 @@ pub fn all_presets() -> Vec { ModelPreset { tier: ModelTier::Ram16PlusGb, label: "16 GB+", - description: "Best local quality with Gemma 4 on higher-end devices.", - chat_model_id: "gemma4:e4b", - vision_model_id: "gemma4:e4b", + description: "Best local quality with Gemma 3n on higher-end devices.", + // GH #5055: was `gemma4:e4b`, which does not exist on the Ollama + // library — the tier shipped a model no `ollama pull` could fetch. + // Gemma 3n is the real publication of the "e4b" (effective-4B) + // variant, and `e4b-it-q8_0` (9.5 GB) is what `approx_download_gb` + // below was sized against. + chat_model_id: "gemma3n:e4b-it-q8_0", + vision_model_id: "gemma3n:e4b-it-q8_0", embedding_model_id: "nomic-embed-text:latest", - quantization: "qat", + // The other tiers ship QAT builds; this one is q8_0. The field is a + // display label (`effective_quantization`) and does not take part in + // resolving the model tag, but it should still match what is pulled. + quantization: "q8_0", vision_mode: VisionMode::Bundled, supports_screen_summary: true, target_ram_gb: 16, @@ -423,4 +431,36 @@ mod tests { apply_preset_to_config(&mut config, ModelTier::Ram16PlusGb); assert_eq!(vision_mode_for_config(&config), VisionMode::Bundled); } + + /// GH #5055: every preset must name a model that actually exists on the + /// Ollama library. The 16 GB+ tier shipped `gemma4:e4b` for chat and vision + /// — there is no `gemma4` namespace, so the tier offered a model that no + /// `ollama pull` could ever fetch. Guard the whole table, not just that row. + #[test] + fn presets_reference_no_unpullable_gemma4_namespace() { + for preset in all_presets() { + for (field, id) in [ + ("chat", preset.chat_model_id), + ("vision", preset.vision_model_id), + ("embedding", preset.embedding_model_id), + ] { + assert!( + !id.starts_with("gemma4:"), + "preset {:?} {field} model `{id}` is not a real Ollama model \ + — there is no gemma4 namespace", + preset.tier + ); + } + } + } + + /// The 16 GB+ tier's chat model must be the allowlisted Gemma 3n build, so + /// selecting the preset does not immediately get redirected back to the + /// default by `enforce_mvp_chat_allowlist`. + #[test] + fn high_tier_preset_uses_the_allowlisted_gemma3n_build() { + let preset = preset_for_tier(ModelTier::Ram16PlusGb).expect("16 GB+ preset"); + assert_eq!(preset.chat_model_id, "gemma3n:e4b-it-q8_0"); + assert_eq!(preset.vision_model_id, "gemma3n:e4b-it-q8_0"); + } } diff --git a/tests/raw_coverage/inference_local_admin_raw_coverage_e2e.rs b/tests/raw_coverage/inference_local_admin_raw_coverage_e2e.rs index dddaa8fc9..dd39563c0 100644 --- a/tests/raw_coverage/inference_local_admin_raw_coverage_e2e.rs +++ b/tests/raw_coverage/inference_local_admin_raw_coverage_e2e.rs @@ -96,7 +96,7 @@ async fn local_admin_covers_assets_diagnostics_downloads_and_ops_errors() { config.local_ai.runtime_enabled = true; config.local_ai.opt_in_confirmed = true; config.local_ai.base_url = Some(base.clone()); - config.local_ai.chat_model_id = "gemma4:e4b-it-q8_0".to_string(); + config.local_ai.chat_model_id = "gemma3n:e4b-it-q8_0".to_string(); config.local_ai.embedding_model_id = "bge-m3".to_string(); config.local_ai.vision_model_id = "missing-vision".to_string(); config.local_ai.selected_tier = Some("custom".to_string()); @@ -134,7 +134,7 @@ async fn local_admin_covers_assets_diagnostics_downloads_and_ops_errors() { .as_array() .unwrap() .iter() - .any(|issue| issue.as_str().unwrap().contains("gemma4:e4b-it-q8_0"))); + .any(|issue| issue.as_str().unwrap().contains("gemma3n:e4b-it-q8_0"))); let assets = service.assets_status(&config).await.expect("assets"); assert!(assets.ollama_available); @@ -175,7 +175,7 @@ async fn local_admin_covers_assets_diagnostics_downloads_and_ops_errors() { .lock() .expect("models") .iter() - .any(|m| m == "gemma4:e4b-it-q8_0")); + .any(|m| m == "gemma3n:e4b-it-q8_0")); let mut lm_config = config.clone(); lm_config.local_ai.provider = "lmstudio".to_string(); @@ -220,7 +220,7 @@ async fn local_admin_covers_assets_diagnostics_downloads_and_ops_errors() { .await .expect("ops progress") .value; - assert_eq!(ops_progress.chat.id, "gemma4:e4b-it-q8_0"); + assert_eq!(ops_progress.chat.id, "gemma3n:e4b-it-q8_0"); let ops_asset = local_ai_download_asset(&config, "embedding") .await @@ -529,7 +529,7 @@ async fn ollama_show(Json(body): Json) -> impl IntoResponse { .into_response(); } let context = match model { - "gemma4:e4b-it-q8_0" => 1024, + "gemma3n:e4b-it-q8_0" => 1024, "bge-m3" => 8192, _ => 4096, }; @@ -545,7 +545,7 @@ async fn ollama_show(Json(body): Json) -> impl IntoResponse { async fn ollama_pull(State(state): State, Json(body): Json) -> impl IntoResponse { let name = body["name"] .as_str() - .unwrap_or("gemma4:e4b-it-q8_0") + .unwrap_or("gemma3n:e4b-it-q8_0") .to_string(); state.ollama_models.lock().expect("models").push(name); let body = [ diff --git a/tests/raw_coverage/inference_provider_admin_round22_raw_coverage_e2e.rs b/tests/raw_coverage/inference_provider_admin_round22_raw_coverage_e2e.rs index 4d789f6b5..99a69967b 100644 --- a/tests/raw_coverage/inference_provider_admin_round22_raw_coverage_e2e.rs +++ b/tests/raw_coverage/inference_provider_admin_round22_raw_coverage_e2e.rs @@ -295,7 +295,7 @@ async fn local_admin_covers_diagnostics_errors_assets_status_and_shutdown_with_f config.local_ai.runtime_enabled = true; config.local_ai.opt_in_confirmed = true; config.local_ai.base_url = Some(base.clone()); - config.local_ai.chat_model_id = "gemma4:e4b-it-q8_0".to_string(); + config.local_ai.chat_model_id = "gemma3n:e4b-it-q8_0".to_string(); config.local_ai.embedding_model_id = "all-minilm:latest".to_string(); config.local_ai.selected_tier = Some("custom".to_string()); config.local_ai.preload_embedding_model = true; @@ -325,7 +325,7 @@ async fn local_admin_covers_diagnostics_errors_assets_status_and_shutdown_with_f assert!(issues.iter().any(|issue| issue .as_str() .unwrap() - .contains("Chat model `gemma4:e4b-it-q8_0`"))); + .contains("Chat model `gemma3n:e4b-it-q8_0`"))); assert!(issues.iter().any(|issue| issue .as_str() .unwrap()