refactor(local-inference): use eredu naming without legacy aliases

This commit is contained in:
Jasper Hugo 2026-10-13 11:15:03 +02:00
parent 7f9f023c3c
commit af2737dea9
12 changed files with 68 additions and 111 deletions

View file

@ -118,7 +118,7 @@ local-inference = ["goose/local-inference"]
aws-providers = ["goose/aws-providers"]
cuda = ["goose/cuda", "local-inference"]
vulkan = ["goose/vulkan", "local-inference"]
mlx = ["goose/mlx", "local-inference"]
eredu = ["goose/eredu", "local-inference"]
telemetry = ["goose/telemetry"]
otel = ["goose/otel"]
system-keyring = ["goose/system-keyring"]

View file

@ -15,7 +15,7 @@ workspace = true
default = []
cuda = ["llama-cpp-2/cuda"]
vulkan = ["llama-cpp-2/vulkan"]
mlx = [
eredu = [
"dep:eredu",
"dep:eredu-core",
"dep:eredu-backend-mlx",

View file

@ -12,7 +12,7 @@ pub(super) trait BackendLoadedModel: Send {
fn as_any_mut(&mut self) -> &mut dyn Any;
}
#[cfg_attr(not(feature = "mlx"), allow(dead_code))]
#[cfg_attr(not(feature = "eredu"), allow(dead_code))]
pub(super) struct LocalGenerationRequest<'a> {
pub model_name: String,
pub system: &'a str,

View file

@ -89,14 +89,14 @@ pub(crate) const EREDU_BACKEND_ID: &str = "eredu";
pub(crate) fn unavailable_reason() -> Option<&'static str> {
if !cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Some("The eredu backend in Goose requires Apple silicon macOS.")
} else if !cfg!(feature = "mlx") {
Some("Eredu support was not compiled in. Rebuild with the `mlx` feature.")
} else if !cfg!(feature = "eredu") {
Some("Eredu support was not compiled in. Rebuild with the `eredu` feature.")
} else {
None
}
}
#[cfg(all(feature = "mlx", target_os = "macos", target_arch = "aarch64"))]
#[cfg(all(feature = "eredu", target_os = "macos", target_arch = "aarch64"))]
mod imp {
use std::any::Any;
use std::collections::HashMap;
@ -1041,7 +1041,7 @@ mod imp {
}
}
#[cfg(not(all(feature = "mlx", target_os = "macos", target_arch = "aarch64")))]
#[cfg(not(all(feature = "eredu", target_os = "macos", target_arch = "aarch64")))]
mod imp {
use super::*;
use crate::backend::{BackendLoadedModel, LocalGenerationRequest, LocalInferenceBackend};

View file

@ -8,15 +8,15 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use crate::eredu::snapshot_files_are_complete as mlx_snapshot_files_are_complete;
use crate::eredu::snapshot_files_are_complete;
use crate::huggingface_auth;
const HF_DOWNLOAD_BASE: &str = "https://huggingface.co";
const LLAMACPP_BACKEND_ID: &str = "llamacpp";
const MLX_BACKEND_ID: &str = "eredu";
const EREDU_BACKEND_ID: &str = "eredu";
const GGUF_FORMAT: &str = "gguf";
const MLX_FORMAT: &str = "safetensors";
const MLX_VARIANT_ID: &str = "default";
const SAFETENSORS_FORMAT: &str = "safetensors";
const SAFETENSORS_VARIANT_ID: &str = "default";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HfModelInfo {
@ -1143,10 +1143,10 @@ mod tests {
unsupported_reason: None,
};
let mlx_variant = HfModelVariant {
variant_id: MLX_VARIANT_ID.to_string(),
variant_id: SAFETENSORS_VARIANT_ID.to_string(),
label: "Default".to_string(),
backend_id: MLX_BACKEND_ID.to_string(),
format: MLX_FORMAT.to_string(),
backend_id: EREDU_BACKEND_ID.to_string(),
format: SAFETENSORS_FORMAT.to_string(),
model_id: repo_id.clone(),
download_id: repo_id.clone(),
size_bytes: 8,
@ -1195,7 +1195,7 @@ mod tests {
assert!(models[0]
.variants
.iter()
.any(|variant| variant.backend_id == MLX_BACKEND_ID));
.any(|variant| variant.backend_id == EREDU_BACKEND_ID));
}
fn sibling(filename: &str) -> RepoSibling {
@ -1258,18 +1258,18 @@ mod tests {
}
});
assert!(!mlx_snapshot_files_are_complete(&cached, Some(&index)));
assert!(!snapshot_files_are_complete(&cached, Some(&index)));
cached.insert("model-00002-of-00002.safetensors");
assert!(mlx_snapshot_files_are_complete(&cached, Some(&index)));
assert!(snapshot_files_are_complete(&cached, Some(&index)));
}
#[test]
fn mlx_snapshot_requires_every_numbered_safetensors_shard_without_an_index() {
let mut cached = std::collections::HashSet::from(["model-00001-of-00002.safetensors"]);
assert!(!mlx_snapshot_files_are_complete(&cached, None));
assert!(!snapshot_files_are_complete(&cached, None));
cached.insert("model-00002-of-00002.safetensors");
assert!(mlx_snapshot_files_are_complete(&cached, None));
assert!(snapshot_files_are_complete(&cached, None));
}
#[test]
@ -1277,7 +1277,7 @@ mod tests {
let cached =
std::collections::HashSet::from(["model.safetensors", "vision_model.safetensors"]);
assert!(mlx_snapshot_files_are_complete(&cached, None));
assert!(snapshot_files_are_complete(&cached, None));
}
#[test]
@ -2137,8 +2137,8 @@ fn mlx_variants_from_model_info(
vec![HfModelVariant {
variant_id: variant_id.clone(),
label: mlx_variant_label(&variant_id),
backend_id: MLX_BACKEND_ID.to_string(),
format: MLX_FORMAT.to_string(),
backend_id: EREDU_BACKEND_ID.to_string(),
format: SAFETENSORS_FORMAT.to_string(),
model_id: repo_id.to_string(),
download_id: repo_id.to_string(),
size_bytes,
@ -2243,14 +2243,14 @@ fn mlx_config_support(config: &Option<serde_json::Value>) -> Option<String> {
mlx_config_support_for_value(config)
}
#[cfg(all(feature = "mlx", target_os = "macos", target_arch = "aarch64"))]
#[cfg(all(feature = "eredu", target_os = "macos", target_arch = "aarch64"))]
fn mlx_config_support_for_value(config: &serde_json::Value) -> Option<String> {
eredu_architectures::configuration::resolve_model_config(config)
.err()
.map(|error| error.to_string())
}
#[cfg(not(all(feature = "mlx", target_os = "macos", target_arch = "aarch64")))]
#[cfg(not(all(feature = "eredu", target_os = "macos", target_arch = "aarch64")))]
fn mlx_config_support_for_value(_config: &serde_json::Value) -> Option<String> {
None
}
@ -2367,11 +2367,11 @@ fn mlx_variant_id(repo_id: &str, config: &Option<serde_json::Value>) -> String {
.and_then(|config| config.get("torch_dtype"))
.and_then(|value| value.as_str())
.map(|dtype| dtype.replace("float", "f"))
.unwrap_or_else(|| MLX_VARIANT_ID.to_string())
.unwrap_or_else(|| SAFETENSORS_VARIANT_ID.to_string())
}
fn mlx_variant_label(variant_id: &str) -> String {
if variant_id == MLX_VARIANT_ID {
if variant_id == SAFETENSORS_VARIANT_ID {
"SafeTensors".to_string()
} else {
format!("SafeTensors {}", variant_id.to_uppercase())
@ -2384,8 +2384,8 @@ pub async fn resolve_local_model_selection(
variant_id: Option<&str>,
) -> Result<ResolvedLocalModel> {
match backend_id {
MLX_BACKEND_ID | "mlx" => {
resolve_mlx_model(repo_id, variant_id.unwrap_or(MLX_VARIANT_ID)).await
EREDU_BACKEND_ID => {
resolve_mlx_model(repo_id, variant_id.unwrap_or(SAFETENSORS_VARIANT_ID)).await
}
LLAMACPP_BACKEND_ID => {
let quantization = variant_id.ok_or_else(|| {
@ -2496,7 +2496,7 @@ pub async fn resolve_local_model_spec(spec: &str) -> Result<ResolvedLocalModel>
let variants = get_repo_local_variants(spec).await?;
let mlx_variants: Vec<_> = variants
.iter()
.filter(|variant| variant.backend_id == MLX_BACKEND_ID)
.filter(|variant| variant.backend_id == EREDU_BACKEND_ID)
.collect();
if mlx_variants.len() == 1
&& !variants
@ -2907,7 +2907,7 @@ pub async fn cached_local_models() -> Result<Vec<CachedLocalModel>> {
if is_mlx_compatible_repo(&config, &repo_siblings)
&& is_mlx_runtime_supported(&config)
&& valid_index
&& mlx_snapshot_files_are_complete(&cached_filenames, index.as_ref())
&& snapshot_files_are_complete(&cached_filenames, index.as_ref())
{
let cached_files: Vec<_> = revision
.files
@ -2918,9 +2918,9 @@ pub async fn cached_local_models() -> Result<Vec<CachedLocalModel>> {
let model = CachedLocalModel {
id: repo.repo_id.clone(),
repo_id: repo.repo_id.clone(),
filename: MLX_VARIANT_ID.to_string(),
filename: SAFETENSORS_VARIANT_ID.to_string(),
quantization: mlx_variant_id(&repo.repo_id, &config),
backend_id: MLX_BACKEND_ID.to_string(),
backend_id: EREDU_BACKEND_ID.to_string(),
model_path: revision.snapshot_path.clone(),
size_bytes: cached_files.iter().map(|file| file.size_on_disk).sum(),
mmproj_path: None,
@ -3029,7 +3029,7 @@ pub async fn delete_cached_local_model(model_id: &str) -> Result<()> {
.flat_map(|repo| &repo.revisions)
.flat_map(|revision| &revision.files)
.filter(|file| {
if model.backend_id == MLX_BACKEND_ID {
if model.backend_id == EREDU_BACKEND_ID {
should_download_for_mlx(&file.file_name)
} else {
let is_model_weight = file.file_name.ends_with(".gguf")

View file

@ -184,7 +184,12 @@ impl InferenceRuntime {
let state = slot.state.lock().await;
if let ModelSlotState::Loaded { resolved, .. } = &*state {
if selected_backend(&resolved.model_path, backend_id).ok()
== Some(key_backend(&resolved.backend_id))
== Some(
resolved
.backend_id
.as_deref()
.unwrap_or(LLAMACPP_BACKEND_ID),
)
{
return Some(resolved.as_ref().clone());
}
@ -264,14 +269,6 @@ struct ExplicitModelPath {
model_path: PathBuf,
}
fn key_backend(id: &Option<String>) -> &str {
match id.as_deref() {
Some("mlx") => EREDU_BACKEND_ID,
Some(id) => id,
None => LLAMACPP_BACKEND_ID,
}
}
fn selected_backend(path: &Path, requested: Option<&str>) -> Result<&'static str> {
let gguf = has_extension(path, "gguf");
match requested {
@ -280,7 +277,7 @@ fn selected_backend(path: &Path, requested: Option<&str>) -> Result<&'static str
} else {
EREDU_BACKEND_ID
}),
Some("eredu" | "mlx") => Ok(EREDU_BACKEND_ID),
Some("eredu") => Ok(EREDU_BACKEND_ID),
Some("llamacpp") if gguf => Ok(LLAMACPP_BACKEND_ID),
Some("llamacpp") => {
bail!("llama.cpp requires a GGUF checkpoint; use eredu for SafeTensors")
@ -326,14 +323,14 @@ fn explicit_model_path(model_id: &str) -> Result<Option<ExplicitModelPath>> {
return Ok(Some(ExplicitModelPath { model_path: path }));
}
let mlx_directory = if path.is_dir() {
let safetensors_directory = if path.is_dir() {
Some(path.as_path())
} else if path.is_file() && has_extension(&path, "safetensors") {
path.parent()
} else {
None
};
if let Some(directory) = mlx_directory {
if let Some(directory) = safetensors_directory {
if is_safetensors_model_directory(directory)? {
eredu::validate_model_directory(directory)
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
@ -761,7 +758,7 @@ impl ProviderDescriptor for LocalInferenceProvider {
ProviderMetadata::new(
PROVIDER_NAME,
"Local Inference",
"Local inference using GGUF (llama.cpp) and MLX models",
"Local inference using llama.cpp and Eredu",
"",
vec![],
"https://github.com/utilityai/llama-cpp-rs",
@ -1080,7 +1077,7 @@ mod tests {
assert_eq!(selected_backend(gguf, None).unwrap(), "llamacpp");
assert_eq!(selected_backend(safetensors, None).unwrap(), "eredu");
assert_eq!(selected_backend(gguf, Some("eredu")).unwrap(), "eredu");
assert_eq!(selected_backend(safetensors, Some("mlx")).unwrap(), "eredu");
assert!(selected_backend(safetensors, Some("mlx")).is_err());
assert!(selected_backend(safetensors, Some("llamacpp")).is_err());
assert!(selected_backend(gguf, Some("unknown")).is_err());
}

View file

@ -132,7 +132,7 @@ pub async fn download_model(
) -> Result<LocalInferenceModelDownloadResponse> {
let selection = explicit_model_selection(&req)?;
let model_id = local_model_id_from_request(&req, selection.as_ref()).await?;
if matches!(req.backend_id.as_deref(), Some("eredu" | "mlx"))
if req.backend_id.as_deref() == Some("eredu")
&& selection
.as_ref()
.is_some_and(|selection| selection.backend_id == "llamacpp")
@ -251,10 +251,7 @@ fn settings_model_path(model_id: &str) -> Result<std::path::PathBuf> {
}
pub fn get_model_settings(model_id: &str) -> Result<LocalInferenceModelSettingsReadResponse> {
let mut settings = crate::config_resolver::model_settings(model_id)?;
if settings.backend_id.as_deref() == Some("mlx") {
settings.backend_id = Some("eredu".into());
}
let settings = crate::config_resolver::model_settings(model_id)?;
let path = settings_model_path(model_id)?;
let default_backend = crate::selected_backend(&path, None)?.to_string();
let mut available_backends = Vec::new();
@ -588,7 +585,7 @@ fn explicit_model_selection(
) -> Result<Option<LocalModelSelection>> {
let format_backend = match req.format.as_deref() {
Some("gguf") => Some("llamacpp"),
Some("safetensors" | "mlx-safetensors") => Some("eredu"),
Some("safetensors") => Some("eredu"),
Some(format) => anyhow::bail!("Unknown local checkpoint format '{format}'"),
None => None,
};
@ -607,12 +604,12 @@ fn explicit_model_selection(
let is_gguf = format_backend == Some("llamacpp") || parsed_variant_id.is_some();
let variant_id = req.variant_id.clone().or(parsed_variant_id);
match backend_id {
"eredu" | "mlx" if is_gguf => Ok(Some(LocalModelSelection {
"eredu" if is_gguf => Ok(Some(LocalModelSelection {
repo_id,
backend_id: "llamacpp".into(),
variant_id: variant_id.map(|id| hf_models::canonicalize_quantization(&id)),
})),
"mlx" | "eredu" => Ok(Some(LocalModelSelection {
"eredu" => Ok(Some(LocalModelSelection {
repo_id,
backend_id: backend_id.to_string(),
variant_id,
@ -636,7 +633,7 @@ async fn local_model_id_from_request(
) -> Result<String> {
if let Some(selection) = selection {
return match selection.backend_id.as_str() {
"mlx" | "eredu" => Ok(selection.repo_id.clone()),
"eredu" => Ok(selection.repo_id.clone()),
"llamacpp" => {
let quantization = selection.variant_id.as_deref().ok_or_else(|| {
anyhow!(
@ -664,11 +661,11 @@ async fn local_model_id_from_request(
let has_llamacpp = variants
.iter()
.any(|variant| variant.backend_id == "llamacpp");
let mlx_variants: Vec<_> = variants
let eredu_variants: Vec<_> = variants
.iter()
.filter(|variant| variant.backend_id == "eredu")
.collect();
if mlx_variants.len() == 1 && !has_llamacpp {
if eredu_variants.len() == 1 && !has_llamacpp {
Ok(req.spec.clone())
} else {
anyhow::bail!(

View file

@ -6,23 +6,23 @@
use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag};
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
use goose_provider_types::conversation::message::{Message, MessageContent};
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
use rmcp::model::{CallToolRequestParams, Tool};
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
use serde_json::json;
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
use std::borrow::Cow;
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
use uuid::Uuid;
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
pub(crate) const SHELL_TOOL: &str = "developer__shell";
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
pub(crate) const CODE_EXECUTION_TOOL: &str = "code_execution__execute_typescript";
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
pub(crate) fn load_tiny_model_prompt() -> String {
use std::env;
@ -55,7 +55,7 @@ pub(crate) fn load_tiny_model_prompt() -> String {
})
}
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
pub(crate) fn build_emulator_tool_description(tools: &[Tool], code_mode_enabled: bool) -> String {
let mut tool_desc = String::new();
@ -466,7 +466,7 @@ impl StreamingEmulatorParser {
}
}
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
pub(crate) fn message_for_emulator_action(
action: &EmulatorAction,
message_id: &str,
@ -721,7 +721,7 @@ mod tests {
}
}
#[cfg(feature = "mlx")]
#[cfg(feature = "eredu")]
#[test]
fn tool_description_uses_execute_typescript_fence() {
let description = build_emulator_tool_description(&[], true);

View file

@ -17,7 +17,7 @@ local-inference = ["dep:goose-local-inference"]
local-inference-hf-hub = ["local-inference", "goose-local-inference/hf-hub"]
cuda = ["local-inference", "goose-local-inference/cuda"]
vulkan = ["local-inference", "goose-local-inference/vulkan"]
mlx = ["local-inference", "goose-local-inference/mlx"]
eredu = ["local-inference", "goose-local-inference/eredu"]
rustls-tls = [
"reqwest/rustls",
"rmcp/reqwest",

View file

@ -54,7 +54,7 @@ aws-providers = [
]
cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "goose-providers/cuda"]
vulkan = ["local-inference", "goose-providers/vulkan"]
mlx = ["local-inference", "goose-providers/mlx"]
eredu = ["local-inference", "goose-providers/eredu"]
rustls-tls = [
"dep:rustls",
"dep:rcgen",

View file

@ -20,7 +20,6 @@ activities:
- Validate ACP thinking-effort discovery, forwarding, and provider switching
- Validate GDK observability hook lifecycle events and payload opt-in
- Validate capped and uncapped auto-compaction thresholds and Desktop editing
- Validate local eredu inference, tool round trips, and interruption recovery
- Test error boundaries including nested delegation prevention
- Generate comprehensive test report
@ -29,7 +28,7 @@ parameters:
input_type: string
requirement: optional
default: "all"
description: "Which test phases to run: all, basic, extensions, delegation, declared-vision, reasoning, zai-coding-plan, acp-effort, compaction, local-inference, advanced"
description: "Which test phases to run: all, basic, extensions, delegation, declared-vision, reasoning, zai-coding-plan, acp-effort, compaction, advanced"
- key: test_depth
input_type: string
@ -59,41 +58,6 @@ instructions: |
You are testing yourself - a running goose instance validating its own capabilities through meta-testing.
This is true first-person integration testing where you use your own tools to test your own functionality.
## Local inference migration checks
When test_phases includes local-inference, or includes all and this session uses
the local provider, validate the currently selected cached model without downloading
new weights. Record the model ID, format, selected backend, and agent-loop mode.
Use a tool to write a small file, read it back in a later turn, and confirm that
tool arguments and results survive the round trip. Request a response long enough
to observe streaming, interrupt it, and verify a subsequent request still works.
Check that reasoning is separated from the visible answer and that completed
native or emulated tool calls execute only once. Record unavailable hardware,
models, or interruption controls as skipped checks, not successful checks.
Repeat this phase with a supported SafeTensors model using the automatic eredu
default and a cached GGUF explicitly set to eredu, then restore the GGUF to its
automatic llama.cpp default. Confirm switching engines does not download weights.
With a cached eredu vision model, send image attachments of different sizes and
check prompt usage against eredu's prepared-input model-position count. Confirm
that text plus media consumes the configured context budget, including rejection
of an oversized input, and that image_token_estimate does not change that count.
Skip these image checks explicitly if no compatible cached vision model exists.
Inspect the eredu inference response log for memory_forecast on ordinary and,
when an already-cached compatible drafter is available, speculative requests.
Confirm the forecast uses the actual input/output budget and preserves unknown
bounds. A forecast error must be diagnostic only, allowing valid generation to
continue. Record unavailable log access or draft models as skipped checks.
Repeat a request with a cached mixed-precision GGUF and inspect the loaded
forecasts before and after warm-up. Check each reported conversion-retention
group's effective policy and that retained payload plus reservations stays within
its bound (256 MiB by default); unavailable observations are not zero usage.
Target and external drafter budgets are separate. Reset may preserve admitted
conversions. Do not add their payload to residency a second time or treat either
the conversion-retention budget or allocator-cache ceiling as a total-memory
limit: unsupported projection layouts, arithmetic or row counts may still need
temporary full-weight casts. Supported multi-row Metal prefill can now avoid them.
Skip this comparison if no suitable checkpoint or forecast log is available.
Run the same tool round trip with both legacy and GOOSE_STATE_MACHINE=1 sessions.
## Understanding First-Person Integration Testing
This is a crucial distinction - as a running goose instance, you are testing yourself using your own capabilities.
This is meta-testing in the truest sense: not unit tests or external test harnesses, but you using your tools

View file

@ -472,8 +472,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
save({ ...settings, [key]: value });
};
const effectiveBackend =
settings.backendId === 'mlx' ? 'eredu' : settings.backendId || defaultBackend;
const effectiveBackend = settings.backendId || defaultBackend;
const usesEredu = effectiveBackend === 'eredu';
const samplingType: SamplingType = settings.sampling?.type ?? 'Temperature';
@ -571,7 +570,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
<SelectField<string>
label={intl.formatMessage(i18n.inferenceBackend)}
description={intl.formatMessage(i18n.inferenceBackendDescription)}
value={settings.backendId === 'mlx' ? 'eredu' : settings.backendId || 'auto'}
value={settings.backendId || 'auto'}
options={[
{ value: 'auto', label: intl.formatMessage(i18n.automaticBackend) },
...Array.from(