use crate::records::{Capability, JsonValue, ModelRecord, ParamSpec, ParamType, RuntimeId};
type Matcher = Box<dyn Fn(&ModelRecord) -> bool + Send + Sync>;
pub struct ModelProfile {
pub id: String,
pub schema: Vec<ParamSpec>,
matcher: Matcher,
}
impl ModelProfile {
pub fn new(
id: &str,
schema: Vec<ParamSpec>,
matcher: impl Fn(&ModelRecord) -> bool + Send + Sync + 'static,
) -> Self {
Self {
id: id.to_owned(),
schema,
matcher: Box::new(matcher),
}
}
pub fn matches(&self, record: &ModelRecord) -> bool {
(self.matcher)(record)
}
}
pub struct ProfileRegistry {
pub profiles: Vec<ModelProfile>,
}
impl ProfileRegistry {
pub fn new(profiles: Vec<ModelProfile>) -> Self {
Self { profiles }
}
pub fn schema(&self, record: &ModelRecord) -> Vec<ParamSpec> {
let mut specs: Vec<ParamSpec> = Vec::new();
for profile in &self.profiles {
if profile.matches(record) {
for spec in &profile.schema {
if !specs.iter().any(|kept| kept.key == spec.key) {
specs.push(spec.clone());
}
}
}
}
if !specs.iter().any(|spec| spec.key == "context_length")
&& let Some(context) = context_length_spec(record)
{
specs.push(context);
}
specs
}
pub fn refreshed(&self, record: &ModelRecord) -> ModelRecord {
let schema = self.schema(record);
if schema.is_empty() {
return record.clone();
}
let mut updated = record.clone();
updated.params = schema;
updated
}
pub fn builtin() -> Self {
Self::new(vec![
ModelProfile::new(
"text-generation",
vec![temperature(), top_p(), max_tokens()],
is_text,
),
runtime_extras(
"sampling-llama-cpp",
RuntimeId::llama_cpp(),
vec![
top_k(),
min_p(),
repeat_penalty(),
frequency_penalty(),
presence_penalty(),
seed(),
stop(),
],
),
runtime_extras(
"sampling-mlx-swift",
RuntimeId::mlx_swift(),
vec![repeat_penalty(), stop()],
),
runtime_extras(
"sampling-mlx-lm",
RuntimeId::mlx_lm(),
vec![top_k(), min_p(), repeat_penalty(), seed(), stop()],
),
runtime_extras(
"sampling-ollama",
RuntimeId::ollama(),
vec![
top_k(),
min_p(),
seed(),
repeat_penalty(),
frequency_penalty(),
presence_penalty(),
stop(),
],
),
runtime_extras(
"sampling-endpoint",
RuntimeId::openai_endpoint(),
vec![stop(), seed(), frequency_penalty(), presence_penalty()],
),
runtime_extras(
"sampling-apple",
RuntimeId::apple_foundation(),
vec![top_k(), seed()],
),
ModelProfile::new(
"speech-synthesis",
vec![plain("voice", ParamType::String), speed()],
|record| record.can(&Capability::speak()),
),
ModelProfile::new(
"transcription",
vec![
plain("language", ParamType::String),
plain("translate", ParamType::Bool),
],
|record| record.can(&Capability::transcribe()),
),
ModelProfile::new(
"togglable-thinking",
vec![plain("thinking", ParamType::Bool)],
|record| {
record.can(&Capability::chat())
&& record.runtime.id.as_ref().is_some_and(is_thinking_runtime)
},
),
])
}
}
pub fn context_length_spec(record: &ModelRecord) -> Option<ParamSpec> {
if !(record.can(&Capability::chat()) || record.can(&Capability::complete())) {
return None;
}
let runtime = record.runtime.id.as_ref()?;
if !is_context_honoring(runtime) {
return None;
}
match record.context_length {
Some(window) if window > 0 => Some(ParamSpec {
key: "context_length".to_owned(),
param_type: ParamType::Int,
default_value: Some(JsonValue::Int(window.min(32768))),
range: Some(vec![
JsonValue::Int(512.min(window)),
JsonValue::Int(window),
]),
values: None,
}),
_ => Some(ParamSpec {
key: "context_length".to_owned(),
param_type: ParamType::Int,
default_value: None,
range: Some(vec![JsonValue::Int(512), JsonValue::Int(131072)]),
values: None,
}),
}
}
fn is_text(record: &ModelRecord) -> bool {
record.can(&Capability::chat()) || record.can(&Capability::complete())
}
fn is_context_honoring(runtime: &RuntimeId) -> bool {
*runtime == RuntimeId::ollama() || *runtime == RuntimeId::llama_cpp()
}
fn is_thinking_runtime(runtime: &RuntimeId) -> bool {
*runtime == RuntimeId::ollama() || *runtime == RuntimeId::mlx_lm()
}
fn runtime_extras(id: &str, runtime: RuntimeId, schema: Vec<ParamSpec>) -> ModelProfile {
ModelProfile::new(id, schema, move |record| {
is_text(record) && record.runtime.id.as_ref() == Some(&runtime)
})
}
fn float(key: &str, low: f64, high: f64) -> ParamSpec {
ParamSpec {
key: key.to_owned(),
param_type: ParamType::Float,
default_value: None,
range: Some(vec![JsonValue::Double(low), JsonValue::Double(high)]),
values: None,
}
}
fn int(key: &str, low: i64, high: i64) -> ParamSpec {
ParamSpec {
key: key.to_owned(),
param_type: ParamType::Int,
default_value: None,
range: Some(vec![JsonValue::Int(low), JsonValue::Int(high)]),
values: None,
}
}
fn plain(key: &str, param_type: ParamType) -> ParamSpec {
ParamSpec {
key: key.to_owned(),
param_type,
default_value: None,
range: None,
values: None,
}
}
fn temperature() -> ParamSpec {
float("temperature", 0.0, 2.0)
}
fn top_p() -> ParamSpec {
float("top_p", 0.0, 1.0)
}
fn top_k() -> ParamSpec {
int("top_k", 0, 100)
}
fn min_p() -> ParamSpec {
float("min_p", 0.0, 1.0)
}
fn max_tokens() -> ParamSpec {
int("max_tokens", 1, 32768)
}
fn repeat_penalty() -> ParamSpec {
float("repeat_penalty", 0.5, 2.0)
}
fn frequency_penalty() -> ParamSpec {
float("frequency_penalty", -2.0, 2.0)
}
fn presence_penalty() -> ParamSpec {
float("presence_penalty", -2.0, 2.0)
}
fn seed() -> ParamSpec {
plain("seed", ParamType::Int)
}
fn stop() -> ParamSpec {
plain("stop", ParamType::String)
}
fn speed() -> ParamSpec {
ParamSpec {
key: "speed".to_owned(),
param_type: ParamType::Float,
default_value: Some(JsonValue::Double(1.0)),
range: Some(vec![JsonValue::Double(0.5), JsonValue::Double(2.0)]),
values: None,
}
}