mod anthropic_profiles;
mod anthropic_capabilities;
mod enumeration;
mod gpt6;
mod model_id_match;
mod realtime;
mod speed;
pub use enumeration::*;
pub use speed::estimate_cost_usd_for_speed;
use speed::{speed_flex_only, speed_flex_priority, speed_priority_only};
use crate::model_profile_data::types::{
CostTier, Modality, ModelCost, ModelLimits, ModelModalities, ModelProfile, ModelVendor,
ReasoningEffort, ReasoningEffortConfig, ReasoningEffortValue, ServiceKind, Verbosity,
VerbosityConfig, VerbosityValue,
};
fn effort(value: ReasoningEffort, name: &str) -> ReasoningEffortValue {
ReasoningEffortValue {
value,
name: name.into(),
}
}
fn reasoning_effort_standard() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
],
default: ReasoningEffort::Medium,
}
}
fn reasoning_effort_high_only() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![effort(ReasoningEffort::High, "High")],
default: ReasoningEffort::High,
}
}
fn reasoning_effort_toggle() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::None, "Off"),
effort(ReasoningEffort::High, "On"),
],
default: ReasoningEffort::None,
}
}
fn reasoning_effort_gpt5_pre51() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
],
default: ReasoningEffort::Medium,
}
}
fn reasoning_effort_gpt51() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::None, "None"),
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
],
default: ReasoningEffort::None,
}
}
fn reasoning_effort_gpt52() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::None, "None"),
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
effort(ReasoningEffort::Xhigh, "Extra High"),
],
default: ReasoningEffort::None,
}
}
fn reasoning_effort_gpt55() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::None, "None"),
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
effort(ReasoningEffort::Xhigh, "Extra High"),
],
default: ReasoningEffort::Medium,
}
}
fn reasoning_effort_realtime() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::Minimal, "Minimal"),
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
effort(ReasoningEffort::Xhigh, "Extra High"),
],
default: ReasoningEffort::Low,
}
}
fn reasoning_effort_gpt52_pro() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
effort(ReasoningEffort::Xhigh, "Extra High"),
],
default: ReasoningEffort::Medium,
}
}
fn reasoning_effort_anthropic_extended_thinking() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::Low, "Low (1K tokens)"),
effort(ReasoningEffort::Medium, "Medium (4K tokens)"),
effort(ReasoningEffort::High, "High (16K tokens)"),
effort(ReasoningEffort::Xhigh, "Extra High (32K tokens)"),
],
default: ReasoningEffort::Medium,
}
}
fn reasoning_effort_anthropic_adaptive_thinking() -> ReasoningEffortConfig {
ReasoningEffortConfig {
values: vec![
effort(ReasoningEffort::Low, "Low"),
effort(ReasoningEffort::Medium, "Medium"),
effort(ReasoningEffort::High, "High"),
effort(ReasoningEffort::Xhigh, "Max"),
],
default: ReasoningEffort::High,
}
}
fn verbosity(value: Verbosity, name: &str) -> VerbosityValue {
VerbosityValue {
value,
name: name.into(),
}
}
fn verbosity_standard() -> VerbosityConfig {
VerbosityConfig {
values: vec![
verbosity(Verbosity::Low, "Low"),
verbosity(Verbosity::Medium, "Medium"),
verbosity(Verbosity::High, "High"),
],
default: Verbosity::Medium,
}
}
struct ModelDescriptor {
ids: &'static [&'static str],
vendor: ModelVendor,
surfaces: &'static [&'static str],
service: ServiceKind,
}
const fn md(
ids: &'static [&'static str],
vendor: ModelVendor,
surfaces: &'static [&'static str],
) -> ModelDescriptor {
ModelDescriptor {
ids,
vendor,
surfaces,
service: ServiceKind::Chat,
}
}
const fn md_service(
ids: &'static [&'static str],
vendor: ModelVendor,
surfaces: &'static [&'static str],
service: ServiceKind,
) -> ModelDescriptor {
ModelDescriptor {
ids,
vendor,
surfaces,
service,
}
}
const OPENAI: &[&str] = &["openai", "openrouter", "azure_openai", "openai_completions"];
const OPENAI_COMPAT: &[&str] = &["openai", "openrouter", "openai_completions"];
const ANTHROPIC: &[&str] = &["anthropic"];
const GEMINI: &[&str] = &["gemini"];
const LLMSIM: &[&str] = &["llmsim"];
const MICROSOFT_MAI: &[&str] = &["mai", "openai", "openrouter", "openai_completions"];
const META_MUSE: &[&str] = &["meta", "openai", "openrouter", "openai_completions"];
const MISTRAL: &[&str] = &["mistral", "openrouter", "openai_completions"];
static REGISTRY: &[ModelDescriptor] = &[
md_service(
&["jev-1.13.0", "jev-1.13", "typesafe/jev-1.13"],
ModelVendor::TypeSafe,
&["typesafe", "openrouter"],
ServiceKind::Decisions,
),
md_service(
&["jev-latest", "typesafe/jev-latest"],
ModelVendor::TypeSafe,
&["typesafe", "openrouter"],
ServiceKind::Decisions,
),
md_service(
&["gpt-6-luna-decisions"],
ModelVendor::OpenAi,
&["openai"],
ServiceKind::Decisions,
),
md_service(
&["text-embedding-3-small"],
ModelVendor::OpenAi,
OPENAI,
ServiceKind::Embeddings,
),
md_service(
&["text-embedding-3-large"],
ModelVendor::OpenAi,
OPENAI,
ServiceKind::Embeddings,
),
md_service(
&["gpt-realtime-2"],
ModelVendor::OpenAi,
OPENAI,
ServiceKind::Realtime,
),
md_service(
&["gpt-realtime-2.1"],
ModelVendor::OpenAi,
OPENAI,
ServiceKind::Realtime,
),
md(&["o3"], ModelVendor::OpenAi, OPENAI),
md(&["o3-pro"], ModelVendor::OpenAi, OPENAI),
md(&["o3-deep-research"], ModelVendor::OpenAi, OPENAI),
md(&["o4-mini"], ModelVendor::OpenAi, OPENAI),
md(&["o4-mini-deep-research"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-4.1"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-4.1-mini"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-4.1-nano"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5-mini"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5-nano"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5-pro"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5-codex"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5-chat-latest"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.1"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.1-codex"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.1-codex-mini"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.1-codex-max"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.1-chat-latest"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.2"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.2-pro"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.2-codex"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.2-chat-latest"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.3-codex"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.4"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.4-mini"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.4-nano"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.4-pro"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.5"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.5-pro"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.6-sol", "gpt-5.6"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.6-terra"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-5.6-luna"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-6-astra"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-6-sol"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-6-luna"], ModelVendor::OpenAi, OPENAI),
md(&["gpt-6.1-sol"], ModelVendor::OpenAi, OPENAI),
md(&["claude-fable-5-1"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-fable-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-5-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-8"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-7"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-6"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-fable-5-1[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-fable-5[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-5-5[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-5[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-8[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-7[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-6[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-sonnet-5-5"], ModelVendor::Anthropic, ANTHROPIC),
md(
&["claude-sonnet-5-5[1m]"],
ModelVendor::Anthropic,
ANTHROPIC,
),
md(&["claude-sonnet-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-sonnet-5[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-sonnet-4-6"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-haiku-5-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-haiku-5-5[1m]"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-haiku-4-5"], ModelVendor::Anthropic, ANTHROPIC),
md(&["claude-opus-4"], ModelVendor::Anthropic, ANTHROPIC),
md(&["gemini-3.1-pro-preview"], ModelVendor::Google, GEMINI),
md(&["gemini-3.8-flash"], ModelVendor::Google, GEMINI),
md(&["gemini-3.7-flash"], ModelVendor::Google, GEMINI),
md(&["gemini-3.6-flash"], ModelVendor::Google, GEMINI),
md(&["gemini-3.5-flash"], ModelVendor::Google, GEMINI),
md(&["gemini-3.5-flash-lite"], ModelVendor::Google, GEMINI),
md(&["gemini-3.1-flash-lite"], ModelVendor::Google, GEMINI),
md(&["gemini-2.5-pro"], ModelVendor::Google, GEMINI),
md(&["gemini-2.5-flash"], ModelVendor::Google, GEMINI),
md(&["gemini-2.0-flash"], ModelVendor::Google, GEMINI),
md(
&[
"nemotron-3-super-120b-a12b",
"nvidia/nemotron-3-super-120b-a12b",
],
ModelVendor::Nvidia,
OPENAI_COMPAT,
),
md(
&["qwen3.8-max", "qwen/qwen3.8-max"],
ModelVendor::Qwen,
OPENAI_COMPAT,
),
md(
&["qwen3.7-max", "qwen/qwen3.7-max"],
ModelVendor::Qwen,
OPENAI_COMPAT,
),
md(
&["mai-1-preview", "microsoft/mai-1-preview"],
ModelVendor::Microsoft,
MICROSOFT_MAI,
),
md(
&["mai-code-1-flash", "microsoft/mai-code-1-flash"],
ModelVendor::Microsoft,
MICROSOFT_MAI,
),
md(
&["muse-spark-1.3", "meta/muse-spark-1.3"],
ModelVendor::Meta,
META_MUSE,
),
md(
&[
"muse-spark-1.3-contributor",
"meta/muse-spark-1.3-contributor",
],
ModelVendor::Meta,
META_MUSE,
),
md(
&["muse-spark-1.2", "meta/muse-spark-1.2"],
ModelVendor::Meta,
META_MUSE,
),
md(
&[
"muse-spark-1.2-contributor",
"meta/muse-spark-1.2-contributor",
],
ModelVendor::Meta,
META_MUSE,
),
md(
&["minimax-m3", "minimax/minimax-m3"],
ModelVendor::MiniMax,
OPENAI_COMPAT,
),
md(
&["kimi-k2-thinking", "moonshotai/kimi-k2-thinking"],
ModelVendor::Moonshot,
OPENAI_COMPAT,
),
md(
&["kimi-k3", "moonshotai/kimi-k3"],
ModelVendor::Moonshot,
OPENAI_COMPAT,
),
md(
&[
"mistral-large-4",
"mistral-large-4-0",
"mistralai/mistral-large-4-0",
"mistralai/mistral-large-4",
"mistral/mistral-large-4",
],
ModelVendor::Mistral,
MISTRAL,
),
md(
&[
"mistral-medium-2604",
"mistral-medium-latest",
"mistralai/mistral-medium-3-5",
],
ModelVendor::Mistral,
MISTRAL,
),
md(
&[
"mistral-small-2603",
"mistral-small-latest",
"mistralai/mistral-small-2603",
],
ModelVendor::Mistral,
MISTRAL,
),
md(
&["grok-4.7", "x-ai/grok-4.7", "xai/grok-4.7"],
ModelVendor::XAi,
OPENAI_COMPAT,
),
md(
&["grok-4.3", "x-ai/grok-4.3", "xai/grok-4.3"],
ModelVendor::XAi,
OPENAI_COMPAT,
),
md(&["llmsim-default", "llmsim"], ModelVendor::LlmSim, LLMSIM),
];
fn resolve_descriptor(provider_type: &str, model_id: &str) -> Option<&'static ModelDescriptor> {
let id = model_id.as_bytes();
let mut longest_match = 0;
let mut best_for_surface: Option<&'static ModelDescriptor> = None;
for descriptor in REGISTRY {
for alias in descriptor.ids {
let alias = alias.as_bytes();
if !model_id_match::matches_alias(id, alias) {
continue;
}
if alias.len() > longest_match {
longest_match = alias.len();
best_for_surface = descriptor
.surfaces
.contains(&provider_type)
.then_some(descriptor);
} else if alias.len() == longest_match && descriptor.surfaces.contains(&provider_type) {
best_for_surface = Some(descriptor);
}
}
}
best_for_surface
}
pub fn get_model_profile(provider_type: &str, model_id: &str) -> Option<ModelProfile> {
let descriptor = resolve_descriptor(provider_type, model_id)?;
let mut profile = profile_data(descriptor.ids[0])?;
if provider_type != "openai" && provider_type != "meta" {
profile.supports_phases = false;
}
if provider_type != "openai" && provider_type != "anthropic" && provider_type != "meta" {
profile.tool_search = false;
}
if provider_type != "anthropic" {
profile.supports_server_compaction = false;
}
if provider_type != "openai" {
profile.speed = None;
}
if provider_type != "openai" {
profile.verbosity = None;
}
Some(profile)
}
pub fn estimate_cost_usd(
provider_type: &str,
model_id: &str,
input_tokens: u32,
output_tokens: u32,
cache_read_tokens: u32,
cache_creation_tokens: u32,
) -> Option<f64> {
let cost = get_model_profile(provider_type, model_id)?.cost?;
let prompt_tokens = input_tokens
.saturating_add(cache_read_tokens)
.saturating_add(cache_creation_tokens);
let active_tier = cost
.cost_tiers
.iter()
.filter(|tier| prompt_tokens > tier.above_tokens.max(0) as u32)
.max_by_key(|tier| tier.above_tokens);
let input_rate = active_tier.map_or(cost.input, |tier| tier.input);
let output_rate = active_tier.map_or(cost.output, |tier| tier.output);
let cache_read_rate = active_tier
.and_then(|tier| tier.cache_read)
.or(cost.cache_read)
.unwrap_or(input_rate);
let cache_write_rate = match active_tier {
Some(tier) => tier.cache_write.unwrap_or(input_rate),
None => cost.cache_write.unwrap_or(input_rate),
};
let per_million = |tokens: u32, rate: f64| (tokens as f64 / 1_000_000.0) * rate;
Some(
per_million(input_tokens, input_rate)
+ per_million(cache_read_tokens, cache_read_rate)
+ per_million(cache_creation_tokens, cache_write_rate)
+ per_million(output_tokens, output_rate),
)
}
pub fn get_model_vendor(provider_type: &str, model_id: &str) -> Option<ModelVendor> {
resolve_descriptor(provider_type, model_id).map(|descriptor| descriptor.vendor)
}
pub fn get_model_profile_key(provider_type: &str, model_id: &str) -> Option<String> {
resolve_descriptor(provider_type, model_id)
.map(|descriptor| format!("{}/{}", descriptor.vendor.slug(), descriptor.ids[0]))
}
pub fn get_model_profile_by_key(key: &str) -> Option<ModelProfile> {
let (vendor_slug, canonical) = key.split_once('/')?;
let descriptor = REGISTRY.iter().find(|descriptor| {
descriptor.vendor.slug().eq_ignore_ascii_case(vendor_slug)
&& descriptor.ids[0].eq_ignore_ascii_case(canonical)
})?;
profile_data(descriptor.ids[0])
}
pub fn get_model_service_kind(provider_type: &str, model_id: &str) -> ServiceKind {
resolve_descriptor(provider_type, model_id)
.map(|descriptor| descriptor.service)
.unwrap_or(ServiceKind::Chat)
}
fn profile_data(canonical: &str) -> Option<ModelProfile> {
jev_profile_data(canonical)
.or_else(|| openai_profile_data(canonical))
.or_else(|| anthropic_profile_data(canonical))
.or_else(|| gemini_profile_data(canonical))
.or_else(|| meta_profile_data(canonical))
.or_else(|| third_party_profile_data(canonical))
.or_else(|| llmsim_profile_data(canonical))
}
mod meta_profiles;
use meta_profiles::meta_profile_data;
mod openai_profiles;
use openai_profiles::openai_profile_data;
mod third_party_profiles;
use third_party_profiles::third_party_profile_data;
fn anthropic_1m_variant(mut profile: ModelProfile) -> ModelProfile {
if let Some(limits) = profile.limits.as_mut() {
limits.context = 1_000_000;
}
profile.name = format!("{} (1M)", profile.name);
profile
}
fn anthropic_profile_data(model_id: &str) -> Option<ModelProfile> {
anthropic_profiles::anthropic_profile_data_inner(model_id).map(|mut profile| {
profile.tool_search = anthropic_capabilities::supports_tool_search(&profile.family);
anthropic_capabilities::apply(&mut profile);
profile
})
}
mod gemini_profiles;
use gemini_profiles::gemini_profile_data;
fn llmsim_profile_data(model_id: &str) -> Option<ModelProfile> {
match model_id {
"llmsim-default" | "llmsim" => Some(ModelProfile {
name: "LlmSim Default".into(),
family: "llmsim".into(),
description: None,
release_date: Some("2025-01-01".into()),
last_updated: Some("2025-01-01".into()),
attachment: true,
reasoning: true,
temperature: false, knowledge: Some("2025-08-31".into()),
tool_call: true,
structured_output: true,
open_weights: false,
cost: Some(ModelCost {
input: 0.00, output: 0.00,
cache_read: Some(0.00),
cache_write: None,
cost_tiers: vec![],
}),
limits: Some(ModelLimits {
context: 128_000,
input: None,
output: 64_000,
max_media: None,
}),
modalities: Some(ModelModalities {
input: vec![Modality::Text, Modality::Image],
output: vec![Modality::Text],
}),
reasoning_effort: Some(reasoning_effort_gpt52()), speed: None,
verbosity: None,
tool_search: false,
supported_parameters: Vec::new(),
supports_phases: false,
supports_server_compaction: false,
decisions: None,
}),
_ => None,
}
}
#[cfg(test)]
#[path = "profiles_claude_tests.rs"]
mod claude_tests;
#[cfg(test)]
mod tests;
fn jev_profile_data(canonical: &str) -> Option<ModelProfile> {
if canonical == "gpt-6-luna-decisions" {
return Some(openai_decisions_profile());
}
if !matches!(canonical, "jev-1.13.0" | "jev-latest") {
return None;
}
Some(jev_profile(if canonical == "jev-latest" {
"Jev Latest"
} else {
"Jev 1.13"
}))
}
fn jev_profile(name: &str) -> ModelProfile {
ModelProfile {
name: name.into(),
family: "jev".into(),
description: Some("Typed calibrated decisions over text state.".into()),
release_date: None,
last_updated: None,
attachment: false,
reasoning: false,
temperature: false,
knowledge: None,
tool_call: false,
structured_output: true,
open_weights: false,
cost: Some(crate::model_profile_data::ModelCost::new(0.042, 0.0)),
limits: None,
modalities: None,
reasoning_effort: None,
speed: None,
verbosity: None,
tool_search: false,
supported_parameters: Vec::new(),
supports_phases: false,
supports_server_compaction: false,
decisions: Some(crate::model_profile_data::DecisionModelProfile {
primitives: vec!["noul".into(), "choice".into(), "score".into()],
calibrated: true,
max_choice_options: Some(255),
max_score_levels: Some(10),
state_tokens: Some(32_000),
request_tokens: Some(64_000),
}),
}
}
fn openai_decisions_profile() -> ModelProfile {
let mut profile = jev_profile("GPT-6 Luna Decisions");
profile.family = "gpt-6-luna".into();
profile.description = Some("OpenAI's calibrated typed decisions over text.".into());
profile.cost = Some(crate::model_profile_data::ModelCost::new(0.10, 0.0));
if let Some(decisions) = profile.decisions.as_mut() {
decisions.max_choice_options = None;
decisions.state_tokens = None;
decisions.request_tokens = None;
}
profile
}