use crate::io::api::AuthenticationScheme;
use crate::io::ApiResult;
#[cfg(not(feature = "std"))]
use crate::io::License;
#[cfg(feature = "std")]
use crate::io::License;
use crate::prelude::*;
use crate::prelude::{Error, ErrorKind};
use crate::schema::research_activity::aspect::data::Modality;
use crate::schema::validate::is_partial_date;
use crate::util::{SemanticVersion, ToMarkdown};
use bon::Builder;
use color_eyre::eyre::Report;
use core::{fmt, str::from_utf8};
use derive_more::Display;
use rust_embed::Embed;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::HashMap;
use tera::{Context, Tera};
use validator::{Validate, ValidationError};
pub mod opencode;
#[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,
}
#[derive(Clone, Debug, Display, Deserialize, Serialize, JsonSchema)]
pub enum Metric {
#[display("average pass@1")]
#[serde(rename = "average pass@1")]
AveragePassAt1,
#[display("index")]
#[serde(rename = "index")]
Index,
#[display("pass@1")]
#[serde(rename = "pass@1")]
PassAt1,
#[display("percent correct")]
#[serde(rename = "percent correct")]
PercentCorrect,
#[display("resolve rate")]
#[serde(rename = "resolve rate")]
ResolveRate,
#[display("resolved")]
#[serde(rename = "resolved")]
Resolved,
#[display("score")]
#[serde(rename = "score")]
Score,
#[display("success rate")]
#[serde(rename = "success rate")]
SuccessRate,
}
#[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),
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Validate)]
pub struct Benchmark {
pub name: String,
#[validate(range(min = 0.0))]
pub score: f64,
pub metric: Metric,
#[validate(url)]
pub source: String,
#[validate(custom(function = "is_partial_date"))]
pub date: Option<String>,
pub dataset: Option<String>,
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: 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>,
pub benchmarks: Option<Vec<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 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 variant: Option<String>,
pub version: Option<SemanticVersion>,
pub weights: Option<Vec<Weight>>,
}
#[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,
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct Weight {
pub label: String,
pub url: String,
pub is_open: Option<bool>,
}
impl Default for FrontMatter {
fn default() -> Self {
FrontMatter::init().build()
}
}
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.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.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 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 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 validate_open_weights(details: &ModelDetails) -> Result<(), ValidationError> {
let ModelDetails { open_weights, weights, .. } = details;
let has_open_weight = weights.iter().flatten().any(|w| w.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(())
}
}
#[cfg(test)]
mod tests;