foundation_ai 0.0.2

AI foundation crate for the eweplatform
//! Provider presets - provides pre-configured model presets for various models.
//!
//! The local GGUF presets (`Glm52`, `Gemma4E2b`, …, `with_mtp`) need the
//! `llamacpp` feature; `CloudPresets` and the cloud model ids are always there.
//!
//! Each model is represented by a zero-sized unit struct with methods to create
//! providers with different quantizations. Pass `None` to use default config,
//! or provide your own `HuggingFaceGGUFConfig` for full control.
//!
//! # Example
//!
//! ```ignore
//! use foundation_ai::harness::providers::Glm52;
//!
//! // Create with default config (Q4_K_M quantization)
//! let provider = Glm52::q4_k_m(None)?;
//!
//! // Create with a custom cache dir, defaults for everything else
//! let provider = Glm52::q4_k_m(Some(HuggingFaceGGUFConfig::with_cache_dir("/path/to/cache")))?;
//!
//! // Or full control
//! let config = HuggingFaceGGUFConfig::builder()
//!     .cache_dir("/path/to/cache")
//!     .n_gpu_layers(35)
//!     .build();
//! let provider = Glm52::q4_k_m(Some(config))?;
//! ```
//!
//! For a session on one local model, skip the provider and use
//! `RouterPreset::hugging_face(Glm52::MODEL_ID, config)` (or its
//! `huggingface_cpu` / `huggingface_gpu` forms) — see
//! [`super::RouterPreset::hugging_face`].

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
use std::path::PathBuf;

use foundation_errstacks::ErrorTrace;

use crate::agentic::AgenticError;

use crate::backends::anthropic_messages_provider::AnthropicMessagesProvider;
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
use crate::backends::huggingface_gguf_provider::{HuggingFaceGGUFConfig, HuggingFaceGGUFProvider};
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
use crate::backends::llamacpp::SpeculativeConfig;
use crate::backends::openai_provider::OpenAIProvider;
use crate::backends::openai_responses_provider::ResponsesProvider;

// ===========================================================================
// Quantization presets
// ===========================================================================

pub const Q3_K_M: &str = "Q3_K_M";
pub const Q4_K_M: &str = "Q4_K_M";
pub const Q5_K_M: &str = "Q5_K_M";
pub const Q8_0: &str = "Q8_0";

// ===========================================================================
// Helper function to create a provider
// ===========================================================================

/// The one place a `HuggingFaceGGUFProvider` is constructed for the harness:
/// the family constructors (`Glm52::q4_k_m`, …) and every local
/// [`RouterPreset`](super::RouterPreset) (`hugging_face`, `gemma`,
/// `glm52_gemma`, …) build through it.
///
/// `None` takes [`HuggingFaceGGUFConfig::default()`] (CPU, `q4_k_m`, the
/// `HuggingFace` cache dir).
///
/// # Errors
/// Returns [`AgenticError::Provider`] if the provider cannot be constructed
/// (`HuggingFace` client setup, or the cache dir cannot be created).
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub fn gguf_provider(
    config: Option<HuggingFaceGGUFConfig>,
) -> Result<HuggingFaceGGUFProvider, ErrorTrace<AgenticError>> {
    let config = config.unwrap_or_default();
    let cache_dir = config.cache_dir.clone();
    HuggingFaceGGUFProvider::new(config).map_err(|e| {
        ErrorTrace::new(AgenticError::Provider(format!(
            "failed to create GGUF provider (cache dir {}): {e}",
            cache_dir.display()
        )))
    })
}

/// [`gguf_provider`] at a fixed quantization: the quantization named by the
/// constructor (`q3_k_m`, `q4_k_m`, …) replaces the config's
/// `default_quantization` — otherwise `q3_k_m(Some(cfg))` would silently load
/// the config's default (`q4_k_m`) instead.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
fn create_provider(
    quantization: &str,
    config: Option<HuggingFaceGGUFConfig>,
) -> Result<HuggingFaceGGUFProvider, ErrorTrace<AgenticError>> {
    let mut config = config.unwrap_or_default();
    config.default_quantization = Some(quantization.to_string());
    gguf_provider(Some(config))
}

// ===========================================================================
// MTP / speculative decoding opt-in (spec-51)
// ===========================================================================

/// Enable Multi-Token Prediction (MTP) speculative decoding on a GGUF config.
///
/// Starts from `base` (or the default config) and sets the speculative field.
/// `n_max` is the max draft tokens proposed per target step; `mtp_model` is an
/// optional separate MTP head GGUF (`None` uses the main model's embedded head).
///
/// MTP is **opt-in and capability-gated**: it only takes effect on models that
/// actually ship an MTP head (see each preset's `SUPPORTS_MTP` — currently
/// GLM 5.2, Qwen 3.6, and the Gemma 4 family). Enabling it on a model without
/// an MTP head fails at model load rather than silently doing nothing.
///
/// Pass the result as the *main-model* config to a combo router, e.g.
/// `RouterPreset::glm52_gemma(Some(with_mtp(None, 4, None)), None)`.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
#[must_use]
pub fn with_mtp(
    base: Option<HuggingFaceGGUFConfig>,
    n_max: u32,
    mtp_model: Option<PathBuf>,
) -> HuggingFaceGGUFConfig {
    let mut config = base.unwrap_or_default();
    config.llama_config.speculative = Some(SpeculativeConfig::mtp(mtp_model, n_max));
    config
}

// ===========================================================================
// Model presets with quantization methods
// ===========================================================================

/// Declares a model family's constants and quantization constructors from one
/// line, so every family builds its provider through [`gguf_provider`].
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
macro_rules! gguf_model {
    ($ty:ident, $model_id:literal, mtp = $mtp:literal, $mtp_doc:literal) => {
        impl $ty {
            /// `HuggingFace` repo id the GGUF files are downloaded from.
            pub const MODEL_ID: &str = $model_id;
            #[doc = $mtp_doc]
            pub const SUPPORTS_MTP: bool = $mtp;

            /// A provider at `Q3_K_M`; `config` as for [`gguf_provider`], with this
            /// quantization replacing its `default_quantization`.
            ///
            /// # Errors
            /// Returns [`AgenticError::Provider`] if the provider cannot be constructed.
            pub fn q3_k_m(
                config: Option<HuggingFaceGGUFConfig>,
            ) -> Result<HuggingFaceGGUFProvider, ErrorTrace<AgenticError>> {
                create_provider(Q3_K_M, config)
            }

            /// A provider at `Q4_K_M`; `config` as for [`gguf_provider`], with this
            /// quantization replacing its `default_quantization`.
            ///
            /// # Errors
            /// Returns [`AgenticError::Provider`] if the provider cannot be constructed.
            pub fn q4_k_m(
                config: Option<HuggingFaceGGUFConfig>,
            ) -> Result<HuggingFaceGGUFProvider, ErrorTrace<AgenticError>> {
                create_provider(Q4_K_M, config)
            }

            /// A provider at `Q5_K_M`; `config` as for [`gguf_provider`], with this
            /// quantization replacing its `default_quantization`.
            ///
            /// # Errors
            /// Returns [`AgenticError::Provider`] if the provider cannot be constructed.
            pub fn q5_k_m(
                config: Option<HuggingFaceGGUFConfig>,
            ) -> Result<HuggingFaceGGUFProvider, ErrorTrace<AgenticError>> {
                create_provider(Q5_K_M, config)
            }

            /// A provider at `Q8_0`; `config` as for [`gguf_provider`], with this
            /// quantization replacing its `default_quantization`.
            ///
            /// # Errors
            /// Returns [`AgenticError::Provider`] if the provider cannot be constructed.
            pub fn q8_0(
                config: Option<HuggingFaceGGUFConfig>,
            ) -> Result<HuggingFaceGGUFProvider, ErrorTrace<AgenticError>> {
                create_provider(Q8_0, config)
            }
        }
    };
}

/// GLM 5.2 - Zhipu AI's flagship model, excellent for long-horizon tasks.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub struct Glm52;

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
gguf_model!(
    Glm52,
    "unsloth/GLM-5.2-GGUF",
    mtp = true,
    "GLM 5.2 ships an MTP head — MTP speculative decoding is supported."
);

/// Qwen 3.6 35B-A3B - Alibaba's MoE model with excellent reasoning.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub struct Qwen36;

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
gguf_model!(
    Qwen36,
    "unsloth/Qwen3.6-35B-A3B-GGUF",
    mtp = true,
    "Qwen 3.6 ships an MTP head — MTP speculative decoding is supported."
);

/// Ornith 1.0 35B - Fine-tuned model with excellent instruction following.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub struct Ornith10;

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
gguf_model!(
    Ornith10,
    "LordNeel/Ornith-1.0-35B-GGUF-llamacpp-tp1",
    mtp = false,
    "Ornith 1.0 does not ship an MTP head."
);

/// Gemma 4 E4B - Google's small expert model, efficient for memory tasks.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub struct Gemma4E4b;

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
gguf_model!(
    Gemma4E4b,
    "unsloth/gemma-4-E4B-it-GGUF",
    mtp = true,
    "Gemma 4 ships an MTP head — MTP speculative decoding is supported."
);

/// Gemma 4 26B-A4B - Google's MoE model.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub struct Gemma4_26b;

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
gguf_model!(
    Gemma4_26b,
    "unsloth/gemma-4-26B-A4B-it-GGUF",
    mtp = true,
    "Gemma 4 ships an MTP head — MTP speculative decoding is supported."
);

/// Gemma 4 E2B - Smallest variant, suitable for resource-constrained
/// environments and as a fast, cheap memory/reflection model.
///
/// NOTE: This points at `unsloth/gemma-4-E2B-it-GGUF` rather than the
/// `ggml-org` mirror because the unsloth repo ships the full K-quant ladder
/// (`Q3_K_M`/`Q4_K_M`/`Q5_K_M`/`Q8_0`); the `ggml-org` mirror only carries
/// `Q8_0` and `bf16`, so the mid-range quant methods below would fail to
/// resolve a file there.
#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
pub struct Gemma4E2b;

#[cfg(all(feature = "llamacpp", not(target_family = "wasm")))]
gguf_model!(
    Gemma4E2b,
    "unsloth/gemma-4-E2B-it-GGUF",
    mtp = true,
    "Gemma 4 ships an MTP head — MTP speculative decoding is supported."
);

// ===========================================================================
// Cloud provider presets
// ===========================================================================

// ===========================================================================
// Cloud model-id presets
// ===========================================================================
//
// Cloud providers serve a model by the id string passed at generation time
// (`ModelId::Name`), not by anything baked into the provider. These constants
// give callers the canonical ids so a router rule and the agent's
// primary and memory models line up.

/// Claude Opus 4.8 — strongest reasoning, the default "main" cloud model.
pub const CLAUDE_OPUS: &str = "claude-opus-4-8";
/// Claude Sonnet 4.6 — fast + cheap, a good cloud memory/reflection model.
pub const CLAUDE_SONNET: &str = "claude-sonnet-4-6";
/// OpenAI GPT-4o — the default "main" OpenAI model.
pub const OPENAI_GPT4O: &str = "gpt-4o";
/// OpenAI GPT-4o-mini — fast + cheap, a good OpenAI memory/reflection model.
pub const OPENAI_GPT4O_MINI: &str = "gpt-4o-mini";

/// Cloud provider presets - pre-configured providers for cloud models.
pub struct CloudPresets;

impl CloudPresets {
    /// Create an Anthropic Claude Opus provider.
    pub fn claude_opus(
        api_key: &str,
    ) -> Result<AnthropicMessagesProvider, ErrorTrace<AgenticError>> {
        Ok(AnthropicMessagesProvider::api_key(api_key))
    }

    /// Create an Anthropic Claude Sonnet provider.
    pub fn claude_sonnet(
        api_key: &str,
    ) -> Result<AnthropicMessagesProvider, ErrorTrace<AgenticError>> {
        Ok(AnthropicMessagesProvider::api_key(api_key))
    }

    /// Create an OpenAI GPT-4 provider (Chat Completions API).
    pub fn openai_gpt4(api_key: &str) -> Result<OpenAIProvider, ErrorTrace<AgenticError>> {
        Ok(OpenAIProvider::api_key(api_key))
    }

    /// Create an OpenAI GPT-4o provider (Chat Completions API).
    pub fn openai_gpt4o(api_key: &str) -> Result<OpenAIProvider, ErrorTrace<AgenticError>> {
        Ok(OpenAIProvider::api_key(api_key))
    }

    /// Create an OpenAI provider that speaks the **Responses API** (rather than
    /// Chat Completions). Use this for the newer `/v1/responses` surface.
    pub fn openai_responses(api_key: &str) -> Result<ResponsesProvider, ErrorTrace<AgenticError>> {
        Ok(ResponsesProvider::api_key(api_key))
    }
}