use crate::io::api::AuthenticationScheme;
use crate::io::database::schema::{ModelRow, Table};
use crate::io::database::{Database, Operations, Row};
#[cfg(not(feature = "std"))]
use crate::io::License;
#[cfg(feature = "std")]
use crate::io::License;
use crate::io::{ApiResult, ModelListFile, Source};
use crate::prelude::*;
use crate::prelude::{Error, ErrorKind};
use crate::schema::hardware::memory::Memory;
use crate::schema::research_activity::aspect::data::Modality;
use crate::schema::validate::is_partial_date;
use crate::schema::OneOrMany;
use crate::util::constants::app::DEFAULT_HUGGINGFACE_DOMAIN;
use crate::util::{strip_suffixes, Label, SemanticVersion, ToMarkdown};
use bon::Builder;
use color_eyre::eyre::{eyre, Report};
use core::{convert::Infallible, fmt, str::from_utf8, str::FromStr};
use derive_more::Display;
use fancy_regex::Regex;
use lazy_static::lazy_static;
use owo_colors::OwoColorize;
use rust_embed::Embed;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::HashMap;
use strum::{EnumIter, IntoEnumIterator};
use tera::{Context, Tera};
use tracing::warn;
use validator::{Validate, ValidationError};
pub mod opencode;
lazy_static! {
static ref HTTP_URL: Result<Regex, fancy_regex::Error> = Regex::new(r"^https?://");
}
pub(crate) const FALLBACK_MODEL_SUFFIXES: &[&str] = &["-fp8", "-maas"];
#[derive(Clone, Debug, Display, Deserialize, Serialize, JsonSchema)]
pub enum Harness {
#[display("Claude Code")]
#[serde(rename = "Claude Code")]
ClaudeCode,
#[display("Codex")]
#[serde(rename = "Codex")]
Codex,
#[display("Codex CLI")]
#[serde(rename = "Codex CLI")]
CodexCli,
#[display("Cursor CLI")]
#[serde(rename = "Cursor CLI")]
CursorCli,
#[display("Gemini CLI")]
#[serde(rename = "Gemini CLI")]
GeminiCli,
#[display("Mini-SWE-Agent")]
#[serde(rename = "Mini-SWE-Agent")]
MiniSweAgent,
#[display("OpenCode")]
#[serde(rename = "OpenCode")]
OpenCode,
#[display("Terminus-2")]
#[serde(rename = "Terminus-2")]
Terminus2,
#[display("{}", _0)]
#[serde(untagged)]
Other(String),
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub enum Metric {
#[serde(rename = "average pass@1")]
AveragePassAt1,
#[serde(rename = "index")]
Index,
#[serde(rename = "pass@1")]
PassAt1,
#[serde(rename = "percent correct")]
PercentCorrect,
#[serde(rename = "percent resolved")]
PercentResolved,
#[serde(rename = "resolve rate")]
ResolveRate,
#[serde(rename = "resolved")]
Resolved,
#[serde(rename = "score")]
Score,
#[serde(rename = "success rate")]
SuccessRate,
#[serde(untagged)]
Other(String),
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub enum Model {
SLM(ModelDetails),
LLM(ModelDetails),
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub enum PromptFileAsset {
Eli5,
ExtractClaim,
FindGaps,
Summarize,
Teach,
Translate,
Unknown(String),
}
#[derive(Clone, Debug, Display, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
#[display("Alibaba Cloud")]
Alibaba,
#[display("Amazon Web Services")]
Amazon,
#[display("Anthropic")]
Anthropic,
#[display("Azure")]
Azure,
#[display("Baichuan")]
Baichuan,
#[display("Baidu")]
Baidu,
#[display("Cohere")]
Cohere,
#[display("Databricks")]
Databricks,
#[display("DeepSeek")]
DeepSeek,
#[display("Doubao")]
Doubao,
#[display("Google")]
Google,
#[display("Groq")]
Groq,
#[display("IBM")]
IBM,
#[display("Kimi")]
Kimi,
#[display("Meta")]
Meta,
#[display("Minimax")]
Minimax,
#[display("Mistral")]
Mistral,
#[display("Moonshot AI")]
MoonshotAI,
#[display("NVIDIA")]
#[serde(alias = "NVIDIA")]
Nvidia,
#[display("Ollama")]
Ollama,
#[display("OpenAI")]
OpenAI,
#[display("Perplexity")]
Perplexity,
#[display("Qwen")]
Qwen,
#[display("Salesforce")]
Salesforce,
#[display("SAP")]
SAP,
#[display("Sarvam AI")]
Sarvam,
#[display("Stepfun")]
Stepfun,
#[display("Tencent")]
Tencent,
#[display("Together AI")]
TogetherAI,
#[display("xAI")]
XAI,
#[display("Xiaomi")]
Xiaomi,
#[display("Zhipu AI")]
ZhipuAI,
#[display("{}", _0)]
Custom(String),
}
#[allow(non_camel_case_types)]
#[derive(Clone, Debug, Default, Display, EnumIter, PartialEq, Serialize, JsonSchema)]
pub enum Quantization {
#[default]
#[display("Q4_K_M")]
#[serde(rename = "Q4_K_M")]
Q4kM,
#[display("Q2_K")]
#[serde(rename = "Q2_K")]
Q2k,
#[display("Q3_K_S")]
#[serde(rename = "Q3_K_S")]
Q3kS,
#[display("Q3_K_M")]
#[serde(rename = "Q3_K_M")]
Q3kM,
#[display("Q3_K_L")]
#[serde(rename = "Q3_K_L")]
Q3kL,
#[display("Q5_K_M")]
#[serde(rename = "Q5_K_M")]
Q5kM,
#[display("Q6_K")]
#[serde(rename = "Q6_K")]
Q6k,
#[display("Q8_0")]
#[serde(rename = "Q8_0")]
Q8_0,
#[display("F8")]
#[serde(rename = "F8")]
F8,
#[display("F16")]
#[serde(rename = "F16")]
F16,
#[display("BF16")]
#[serde(rename = "BF16")]
BF16,
#[display("IQ4_XS")]
#[serde(rename = "IQ4_XS")]
IQ4_XS,
#[display("{}", _0)]
#[serde(untagged)]
Other(String),
}
impl<'de> Deserialize<'de> for Quantization {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer).map(Self::from)
}
}
impl Quantization {
pub fn from_gguf_filename(filename: &str) -> Option<Self> {
let filename = filename.to_ascii_uppercase();
filename.strip_suffix(".GGUF").and_then(|stem| {
stem.split(['-', '.'])
.find(|part| {
matches!(*part, "F16" | "BF16")
|| part
.strip_prefix('Q')
.is_some_and(|value| value.chars().next().is_some_and(|character| character.is_ascii_digit()) && value.contains('_'))
|| part
.strip_prefix("IQ")
.is_some_and(|value| value.chars().next().is_some_and(|character| character.is_ascii_digit()) && value.contains('_'))
})
.or_else(|| {
stem.split(['-', '.'])
.rev()
.find(|part| part.contains("FP") && part.chars().any(|character| character.is_ascii_digit()))
})
.map(Self::from)
})
}
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
pub struct Benchmark {
pub name: String,
#[validate(range(min = 0.0))]
pub score: f64,
#[serde(default, deserialize_with = "deserialize_metric")]
pub metric: Option<Metric>,
#[validate(url)]
pub source: String,
#[validate(custom(function = "is_partial_date"))]
pub date: Option<String>,
pub dataset: Option<String>,
#[serde(default, deserialize_with = "deserialize_harness")]
pub harness: Option<Harness>,
pub variant: Option<String>,
pub version: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
pub struct CostDetails {
pub input: Option<f64>,
pub output: Option<f64>,
pub cache_read: Option<f64>,
pub cache_write: Option<f64>,
pub reasoning: Option<f64>,
#[serde(rename = "input_audio")]
pub input_audio: Option<f64>,
#[serde(rename = "output_audio")]
pub output_audio: Option<f64>,
pub context_over_200k: Option<Box<CostDetails>>,
pub tiers: Option<Vec<CostTier>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct CostTier {
pub input: f64,
pub output: f64,
pub cache_read: Option<f64>,
pub tier: TierInfo,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Serialize, Deserialize, Validate)]
#[serde(rename_all = "kebab-case")]
#[builder(start_fn = init)]
pub struct FrontMatter {
#[builder(default = String::new())]
pub name: String,
#[builder(default = String::new())]
pub description: String,
pub config: Option<PromptTemplateConfiguration>,
#[validate(nested)]
pub license: Option<License>,
pub compatibility: Option<String>,
pub model: Option<String>,
pub metadata: Option<Vec<(String, String)>>,
pub allowed_tools: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
pub struct LimitDetails {
pub context: u64,
pub output: Option<u64>,
pub input: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
pub struct Modalities {
pub input: Vec<Modality>,
pub output: Vec<Modality>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
#[validate(schema(function = "validate_open_weights", skip_on_field_errors = false))]
pub struct ModelDetails {
pub attachment: Option<bool>,
#[serde(default)]
pub benchmarks: Option<OneOrMany<Benchmark>>,
pub family: Option<String>,
pub id: Option<String>,
#[validate(custom(function = "is_partial_date"))]
pub knowledge: Option<String>,
#[validate(custom(function = "is_partial_date"))]
pub last_updated: Option<String>,
pub limit: Option<LimitDetails>,
pub modalities: Option<Modalities>,
pub cost: Option<CostDetails>,
pub name: Option<String>,
pub open_weights: Option<bool>,
pub parameters: Option<i64>,
pub path: Option<String>,
pub reasoning: Option<bool>,
#[validate(custom(function = "is_partial_date"))]
pub release_date: Option<String>,
pub structured_output: Option<bool>,
pub temperature: Option<bool>,
pub tool_call: Option<bool>,
pub fallback: Option<String>,
pub variant: Option<String>,
pub version: Option<SemanticVersion>,
pub weights: Option<Weights>,
}
#[derive(Clone, Copy, Debug, Display, Eq, PartialEq)]
pub enum ModelResolutionReason {
#[display("model is not open")]
NotOpen,
#[display("no open weight sources are declared")]
NoOpenWeights,
#[display("declared weights do not identify a Hugging Face repository")]
NoHuggingFaceRepository,
#[display("model has no identifier or name")]
MissingIdentifier,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ModelSelector(String);
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ModelSelectors(Vec<ModelSelector>);
#[derive(Embed)]
#[folder = "assets/prompts/"]
pub struct PromptTemplate;
#[derive(Builder, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[builder(start_fn = init)]
pub struct PromptTemplateConfiguration {
pub include_analogy: Option<bool>,
pub include_examples: Option<bool>,
pub include_implicit: Option<bool>,
pub include_practice: Option<bool>,
pub max_items: Option<u32>,
#[builder(default = 300)]
pub max_tokens: u32,
pub max_words: Option<u32>,
pub min_confidence: Option<f32>,
#[builder(default = Vec::new())]
pub stop_sequences: Vec<String>,
pub text: Option<String>,
pub language: Option<String>,
#[builder(default = 0.1)]
pub temperature: f32,
#[builder(default = 10)]
pub top_k: u32,
#[builder(default)]
pub version: SemanticVersion,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, JsonSchema, Validate)]
#[builder(start_fn = init, on(String, into))]
pub struct ProviderDetails {
pub authentication: Option<Vec<AuthenticationScheme>>,
pub description: Option<String>,
#[serde(rename = "doc")]
#[validate(url)]
pub documentation: Option<String>,
#[serde(rename = "api")]
#[validate(url)]
pub endpoint: Option<String>,
pub env: Option<Vec<String>>,
#[validate(custom(function = "is_partial_date"))]
pub established_date: Option<String>,
pub id: Option<String>,
#[validate(custom(function = "is_partial_date"))]
pub last_updated: Option<String>,
#[serde(default, deserialize_with = "deserialize_models")]
pub models: Option<Vec<ModelDetails>>,
pub name: Option<String>,
pub npm: Option<String>,
#[validate(url)]
pub url: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TierInfo {
#[serde(rename = "type")]
pub kind: String,
pub size: u64,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct Weight {
pub label: String,
pub url: String,
pub is_open: Option<bool>,
pub quantization: Option<Quantization>,
pub size: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct WeightGroup {
pub quantization: Quantization,
pub repository: String,
pub revision: String,
pub paths: Vec<String>,
pub size: Option<u64>,
}
#[derive(Clone, Debug, Default)]
pub struct WeightGroups(pub Vec<WeightGroup>);
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
#[serde(transparent)]
pub struct Weights(pub Vec<Weight>);
impl From<&str> for Weight {
fn from(value: &str) -> Self {
Self {
label: "inferred".to_string(),
url: value.to_string(),
is_open: None,
quantization: None,
size: None,
}
}
}
impl Weight {
pub fn parse(self) -> Option<(String, String, String, Quantization, Option<u64>)> {
let prefix = format!("https://{DEFAULT_HUGGINGFACE_DOMAIN}/");
self.quantization.and_then(|quantization| {
self.url.strip_prefix(&prefix).and_then(|relative| {
relative.split_once("/resolve/").and_then(|(repository, remainder)| {
remainder
.split_once('/')
.map(|(revision, path)| (repository.to_string(), revision.to_string(), path.to_string(), quantization, self.size))
})
})
})
}
}
impl WeightGroups {
pub fn select(&self, quantization: &[Quantization], gpu_memory: Option<&Memory>) -> Option<&WeightGroup> {
let allowed = match quantization.is_empty() {
| true => vec![Quantization::Q4kM],
| false => quantization.to_vec(),
};
let selected = allowed.iter().find_map(|allowed| {
self.0
.iter()
.filter(|group| &group.quantization == allowed)
.find(|group| match (gpu_memory, group.size) {
| (Some(memory), Some(size)) => memory.can_contain(size).unwrap_or(false),
| _ => true,
})
});
match selected {
| Some(group) => {
if gpu_memory.is_some() && group.size.is_none() {
warn!(
"=> {} GGUF size metadata is incomplete for '{}'; memory eligibility is unknown and the download will proceed",
Label::CAUTION,
group.repository
);
}
Some(group)
}
| None => {
let requested = allowed.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
if gpu_memory.is_some() && self.0.iter().any(|group| allowed.contains(&group.quantization)) {
warn!(
"=> {} Persisted GGUF variant [{requested}] exceeds the configured GPU memory",
Label::rejected(),
);
} else {
warn!(
"=> {} No persisted GGUF variant matched the exact quantization allowlist [{requested}]",
Label::rejected()
);
}
None
}
}
}
}
impl Weights {
pub fn groups(self) -> WeightGroups {
WeightGroups(self.0.into_iter().filter_map(Weight::parse).fold(Vec::new(), |groups, parsed| {
let (repository, revision, path, quantization, size) = parsed;
match groups
.iter()
.position(|group: &WeightGroup| group.repository == repository && group.revision == revision && group.quantization == quantization)
{
| Some(index) => groups
.into_iter()
.enumerate()
.map(|(position, group)| {
if position == index {
WeightGroup {
paths: group.paths.into_iter().chain([path.clone()]).collect(),
size: match (group.size, size) {
| (Some(total), Some(value)) => total.checked_add(value).or(Some(u64::MAX)),
| _ => None,
},
..group
}
} else {
group
}
})
.collect(),
| None => groups
.into_iter()
.chain([WeightGroup {
quantization,
repository,
revision,
paths: vec![path],
size,
}])
.collect(),
}
}))
}
pub fn has_file_metadata(&self) -> bool {
self.0
.iter()
.any(|weight| weight.quantization.is_some() && weight.url.contains("/resolve/"))
}
pub fn infer_quantization(self, model_id: &str) -> Option<Self> {
let inferred = model_id
.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
.map(Quantization::from)
.find(|candidate| {
Quantization::iter()
.filter(|variant| !matches!(variant, Quantization::Other(_)))
.any(|variant| &variant == candidate)
})
.or_else(|| self.0.iter().find_map(|weight| weight.quantization.clone()));
match (self.0.is_empty(), inferred) {
| (true, None) => None,
| (_, Some(quantization)) if self.0.iter().all(|weight| weight.quantization.is_none()) => Some(Self(
[Weight {
quantization: Some(quantization),
..Weight::from(model_id)
}]
.into_iter()
.chain(self.0)
.collect(),
)),
| _ => Some(self),
}
}
pub fn persist(self, model_id: &str, database_path: Option<PathBuf>) -> ApiResult<()> {
let lookup = ModelRow::init()
.model_id(model_id.to_string())
.build()
.select(database_path.clone(), |row| row.model_id.as_deref() == Some(model_id));
match lookup {
| Ok(Some(row)) => {
let existing = row
.weights
.as_deref()
.and_then(|value| serde_json::from_str::<Weights>(value).ok())
.unwrap_or_default();
let refreshed = Weights(
existing
.0
.into_iter()
.filter(|weight| !(weight.quantization.is_some() && weight.url.contains("/resolve/")))
.chain(self.0)
.collect(),
);
refreshed.serialize().and_then(|weights| {
ModelRow {
weights: Some(weights),
..row
}
.update_weights(database_path)
.map(|_| ())
})
}
| Ok(None) => self.serialize().and_then(|weights| {
Database::<Table>::from_path(database_path)
.insert(ModelRow::init().model_id(model_id.to_string()).weights(weights).build())
.map(|_| ())
}),
| Err(why) => Err(why),
}
}
pub fn serialize(self) -> ApiResult<String> {
serde_json::to_string(&self).map_err(|why| eyre!("Failed to serialize model weights — {why}"))
}
}
impl Default for FrontMatter {
fn default() -> Self {
FrontMatter::init().build()
}
}
impl From<&str> for Harness {
fn from(value: &str) -> Self {
match value {
| "Claude Code" => Self::ClaudeCode,
| "Codex" => Self::Codex,
| "Codex CLI" => Self::CodexCli,
| "Cursor CLI" => Self::CursorCli,
| "Gemini CLI" => Self::GeminiCli,
| "Mini-SWE-Agent" | "mini-swe-agent" => Self::MiniSweAgent,
| "OpenCode" => Self::OpenCode,
| "Terminus-2" => Self::Terminus2,
| _ => Self::Other(value.to_string()),
}
}
}
impl From<String> for Harness {
fn from(value: String) -> Self {
Self::Other(value)
}
}
impl fmt::Display for Metric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
| Self::AveragePassAt1 => write!(f, "average pass@1"),
| Self::Index => write!(f, "index"),
| Self::PassAt1 => write!(f, "pass@1"),
| Self::PercentCorrect => write!(f, "percent correct"),
| Self::PercentResolved => write!(f, "percent resolved"),
| Self::ResolveRate => write!(f, "resolve rate"),
| Self::Resolved => write!(f, "resolved"),
| Self::Score => write!(f, "score"),
| Self::SuccessRate => write!(f, "success rate"),
| Self::Other(value) => write!(f, "{value}"),
}
}
}
impl From<&str> for Metric {
fn from(value: &str) -> Self {
match value {
| "average pass@1" => Self::AveragePassAt1,
| "index" => Self::Index,
| "pass@1" => Self::PassAt1,
| "percent correct" => Self::PercentCorrect,
| "percent resolved" => Self::PercentResolved,
| "resolve rate" => Self::ResolveRate,
| "resolved" => Self::Resolved,
| "score" => Self::Score,
| "success rate" => Self::SuccessRate,
| _ => Self::Other(value.to_string()),
}
}
}
impl From<String> for Metric {
fn from(value: String) -> Self {
Self::Other(value)
}
}
impl ToMarkdown for ModelDetails {
fn to_markdown(&self) -> String {
let lines = [
self.attachment.map(|value| format!("- Attachment: {value}")),
self.family.as_ref().map(|value| format!("- Family: {value}")),
self.id.as_ref().map(|value| format!("- ID: {value}")),
self.knowledge.as_ref().map(|value| format!("- Knowledge: {value}")),
self.last_updated.as_ref().map(|value| format!("- Last Updated: {value}")),
self.name.as_ref().map(|value| format!("- Name: {value}")),
self.open_weights.map(|value| format!("- Open Weights: {value}")),
self.path.as_ref().map(|value| format!("- Path: {value}")),
self.cost.as_ref().map(|c| {
let parts = [
c.input.map(|v| format!("input=${v}")),
c.output.map(|v| format!("output=${v}")),
c.cache_read.map(|v| format!("cache_read=${v}")),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
format!("- Cost: {}", parts.join(", "))
}),
self.parameters.map(|value| format!("- Parameters: {value}B")),
self.reasoning.map(|value| format!("- Reasoning: {value}")),
self.release_date.as_ref().map(|value| format!("- Release Date: {value}")),
self.structured_output.map(|value| format!("- Structured Output: {value}")),
self.temperature.map(|value| format!("- Temperature: {value}")),
self.tool_call.map(|value| format!("- Tool Call: {value}")),
self.fallback.as_ref().map(|value| format!("- Fallback: {value}")),
self.variant.as_ref().map(|value| format!("- Variant: {value}")),
self.version.as_ref().map(|value| format!("- Version: {value}")),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
if lines.is_empty() {
String::new()
} else {
lines.join("\n").to_string()
}
}
}
impl ModelDetails {
pub fn selector(self) -> Result<ModelSelector, ModelResolutionReason> {
let repository = Option::<Source>::from(self.clone())
.map(|source| source.identifier())
.filter(|identifier| HTTP_URL.as_ref().is_ok_and(|regex| !regex.is_match(identifier).unwrap_or(false)));
let identifier = self
.id
.or(self.name)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let selector = match (repository, self.open_weights, self.weights, identifier) {
| (Some(repository), _, _, _) => Ok(repository),
| (None, None | Some(true), None, Some(identifier)) => Ok(identifier),
| (None, Some(false), _, _) => Err(ModelResolutionReason::NotOpen),
| (None, Some(true), None, _) => Err(ModelResolutionReason::NoOpenWeights),
| (None, _, Some(weights), _) if weights.0.is_empty() => Err(ModelResolutionReason::NoOpenWeights),
| (None, _, Some(_), _) => Err(ModelResolutionReason::NoHuggingFaceRepository),
| (None, _, None, None) => Err(ModelResolutionReason::MissingIdentifier),
};
selector.and_then(|value| ModelSelector::new(value).ok_or(ModelResolutionReason::MissingIdentifier))
}
pub fn with_fallback(self, value: &str) -> Self {
Self {
fallback: Some(value.to_string()),
..self
}
}
pub fn with_id(self, value: &str) -> Self {
Self {
id: Some(value.to_string()),
..self
}
}
}
impl ModelSelector {
pub fn new(value: impl Into<String>) -> Option<Self> {
let value = value.into();
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| Self(trimmed.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn fallback_search_name(&self) -> String {
let name = self.0.rsplit('/').next().unwrap_or_default();
let canonical = strip_suffixes(FALLBACK_MODEL_SUFFIXES, name)
.replace("llama-3.1-", "llama-3_1-")
.replace("llama-3.3-", "llama-3_3-")
.replace("v1.5", "v1_5");
match canonical.as_str() {
| "llama-3_1-nemotron-ultra-253b" => format!("{canonical}-v1"),
| _ => canonical,
}
}
}
impl AsRef<str> for ModelSelector {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ModelSelector {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl From<ModelSelector> for String {
fn from(selector: ModelSelector) -> Self {
selector.0
}
}
impl From<&[String]> for ModelSelectors {
fn from(values: &[String]) -> Self {
Self(values.iter().filter_map(|value| ModelSelector::new(value.clone())).collect())
}
}
impl From<Vec<ModelSelector>> for ModelSelectors {
fn from(values: Vec<ModelSelector>) -> Self {
Self(values)
}
}
impl From<Vec<String>> for ModelSelectors {
fn from(values: Vec<String>) -> Self {
Self(values.into_iter().filter_map(ModelSelector::new).collect())
}
}
impl TryFrom<String> for ModelSelectors {
type Error = Report;
fn try_from(content: String) -> Result<Self, Self::Error> {
let trimmed = content.trim();
if trimmed.is_empty() {
Err(eyre!("Model list file cannot be empty"))
} else {
match serde_norway::from_str::<ModelListFile>(trimmed) {
| Ok(file) => file.selectors().require_non_empty(),
| Err(why) if trimmed.starts_with('[') || trimmed.lines().any(|line| line.trim_start().starts_with("- ")) => {
Err(eyre!("Failed to parse model list file as JSON or YAML — {why}"))
}
| Err(_) => Self::from(trimmed.lines().map(str::to_string).collect::<Vec<_>>()).require_non_empty(),
}
}
}
}
impl ModelSelectors {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &ModelSelector> {
self.0.iter()
}
pub fn parse(content: String) -> ApiResult<Self> {
Self::try_from(content)
}
fn require_non_empty(self) -> ApiResult<Self> {
match self.is_empty() {
| true => Err(eyre!("Model list file cannot be empty")),
| false => Ok(self),
}
}
pub async fn resolve(self, source: &Option<String>, offline: bool) -> ApiResult<Self> {
match source {
| Some(source) => Source::read(source, offline)
.await
.and_then(Self::parse)
.map(|file| Self(self.0.into_iter().chain(file.0).collect())),
| None => Ok(self),
}
}
}
impl fmt::Display for PromptFileAsset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
| Self::Eli5 => "eli5.prompt",
| Self::ExtractClaim => "extract-claim.prompt",
| Self::FindGaps => "find-gaps.prompt",
| Self::Summarize => "summarize.prompt",
| Self::Teach => "teach.prompt",
| Self::Translate => "translate.prompt",
| Self::Unknown(value) => value,
};
write!(f, "{value}")
}
}
impl From<&str> for PromptFileAsset {
fn from(value: &str) -> Self {
match value.to_lowercase().as_str() {
| "eli5" | "eli5.prompt" => Self::Eli5,
| "extract-claim" | "extract-claim.prompt" => Self::ExtractClaim,
| "find-gaps" | "find-gaps.prompt" => Self::FindGaps,
| "summarize" | "summarize.prompt" => Self::Summarize,
| "teach" | "teach.prompt" => Self::Teach,
| "translate" | "translate.prompt" => Self::Translate,
| _ => Self::Unknown(value.into()),
}
}
}
impl From<String> for PromptFileAsset {
fn from(value: String) -> Self {
Self::from(value.as_str())
}
}
impl Default for PromptTemplateConfiguration {
fn default() -> Self {
PromptTemplateConfiguration::init().build()
}
}
impl PromptTemplate {
pub fn from_asset(file_name: &str) -> Option<String> {
match Self::get(file_name) {
| Some(value) => from_utf8(value.data.as_ref()).ok().map(String::from),
| None => None,
}
}
pub fn render<T>(asset: T, config: &PromptTemplateConfiguration) -> ApiResult<String>
where
T: Into<PromptFileAsset>,
{
let name = asset.into().to_string();
Self::from_asset(&name)
.ok_or_else(|| Error::new(ErrorKind::NotFound, format!("Prompt template not found — {name}")))
.map_err(Report::from)
.and_then(|template| {
Context::from_serialize(config)
.map_err(Report::from)
.and_then(|context| Tera::one_off(&template, &context, false).map_err(Report::from))
})
}
}
impl From<&str> for Provider {
fn from(value: &str) -> Self {
match value.to_lowercase().as_str() {
| "alibaba" => Self::Alibaba,
| "amazon" => Self::Amazon,
| "anthropic" => Self::Anthropic,
| "azure" => Self::Azure,
| "baichuan" => Self::Baichuan,
| "baidu" => Self::Baidu,
| "cohere" => Self::Cohere,
| "databricks" => Self::Databricks,
| "deepseek" => Self::DeepSeek,
| "doubao" => Self::Doubao,
| "google" => Self::Google,
| "groq" => Self::Groq,
| "ibm" => Self::IBM,
| "kimi" => Self::Kimi,
| "meta" => Self::Meta,
| "minimax" => Self::Minimax,
| "mistral" => Self::Mistral,
| "moonshotai" => Self::MoonshotAI,
| "nvidia" => Self::Nvidia,
| "ollama" => Self::Ollama,
| "openai" => Self::OpenAI,
| "perplexity" => Self::Perplexity,
| "qwen" => Self::Qwen,
| "salesforce" => Self::Salesforce,
| "sap" => Self::SAP,
| "sarvam" => Self::Sarvam,
| "stepfun" => Self::Stepfun,
| "tencent" => Self::Tencent,
| "togetherai" => Self::TogetherAI,
| "xai" => Self::XAI,
| "xiaomi" => Self::Xiaomi,
| "zhipuai" => Self::ZhipuAI,
| _ => Self::Custom(value.into()),
}
}
}
impl From<&str> for Quantization {
fn from(value: &str) -> Self {
let normalized = value.to_ascii_uppercase();
match normalized.as_str() {
| "Q2_K" | "Q2K" => Self::Q2k,
| "Q3_K_S" | "Q3KS" => Self::Q3kS,
| "Q3_K_M" | "Q3KM" => Self::Q3kM,
| "Q3_K_L" | "Q3KL" => Self::Q3kL,
| "Q4_K_M" | "Q4KM" => Self::Q4kM,
| "Q5_K_M" | "Q5KM" => Self::Q5kM,
| "Q6_K" | "Q6K" => Self::Q6k,
| "Q8_0" | "Q80" => Self::Q8_0,
| "F16" => Self::F16,
| "BF16" => Self::BF16,
| "F8" | "FP8" => Self::F8,
| "IQ4_XS" | "IQ4XS" => Self::IQ4_XS,
| _ => Self::Other(value.to_string()),
}
}
}
impl From<String> for Quantization {
fn from(value: String) -> Self {
Self::from(value.as_str())
}
}
impl FromStr for Quantization {
type Err = Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::from(value))
}
}
impl ToMarkdown for ProviderDetails {
fn to_markdown(&self) -> String {
let lines = [
self.endpoint.as_ref().map(|value| format!("- API Endpoint: {value}")),
self.authentication
.as_ref()
.map(|value| format!("- Auth Methods: {}", value.iter().map(|m| m.to_string()).collect::<Vec<_>>().join(", "))),
self.description.as_ref().map(|value| format!("- Description: {value}")),
self.documentation.as_ref().map(|value| format!("- Documentation: {value}")),
self.env.as_ref().map(|value| format!("- Env Vars: {}", value.join(", "))),
self.established_date.as_ref().map(|value| format!("- Established: {value}")),
self.id.as_ref().map(|value| format!("- ID: {value}")),
self.last_updated.as_ref().map(|value| format!("- Last Updated: {value}")),
self.name.as_ref().map(|value| format!("- Name: {value}")),
self.npm.as_ref().map(|value| format!("- NPM: {value}")),
self.url.as_ref().map(|value| format!("- URL: {value}")),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
if lines.is_empty() {
String::new()
} else {
format!("\n{}", lines.join("\n"))
}
}
}
fn deserialize_models<'de, D>(deserializer: D) -> Result<Option<Vec<ModelDetails>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Models {
Map(HashMap<String, ModelDetails>),
Vec(Vec<ModelDetails>),
}
match Option::<Models>::deserialize(deserializer)? {
| Some(Models::Map(map)) => Ok(Some(map.into_values().collect())),
| Some(Models::Vec(vec)) => Ok(Some(vec)),
| None => Ok(None),
}
}
fn deserialize_metric<'de, D>(deserializer: D) -> Result<Option<Metric>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_optional_typed_value(deserializer)
}
fn deserialize_harness<'de, D>(deserializer: D) -> Result<Option<Harness>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_optional_typed_value(deserializer)
}
fn deserialize_optional_typed_value<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: for<'a> From<&'a str> + From<String>,
{
Option::<serde_json::Value>::deserialize(deserializer)
.map(|value| value.filter(|value| !value.is_null()))
.map(|value| value.map(value_to_string_or_other::<T>))
}
fn value_to_string_or_other<T>(value: serde_json::Value) -> T
where
T: for<'a> From<&'a str> + From<String>,
{
match value {
| serde_json::Value::String(value) => T::from(value.as_str()),
| other => serde_json::to_string(&other).map_or_else(|_| T::from(other.to_string()), T::from),
}
}
fn validate_open_weights(details: &ModelDetails) -> Result<(), ValidationError> {
let ModelDetails { open_weights, weights, .. } = details;
let has_open_weight = weights.iter().flat_map(|weights| &weights.0).any(|weight| weight.is_open == Some(true));
if has_open_weight && !open_weights.unwrap_or(false) {
Err(ValidationError::new("open_weights").with_message("open_weights must be true when any weight has is_open: true".into()))
} else {
Ok(())
}
}
impl ModelDetails {
pub fn report(&self) -> (String, Option<String>) {
let id = self.id.as_deref().unwrap_or("unknown").to_string();
let context = self.fallback.as_ref().map(|fb| format!("{} {fb}", "fallback from".italic()));
(id, context)
}
}
#[cfg(test)]
mod tests;