use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct RuntimeModel {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owned_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub created: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq, Eq)]
#[ts(export)]
pub enum CatalogueSource {
#[default]
Anya2a,
ModelsDev,
Manual,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq, Eq)]
#[ts(export)]
pub enum ModelType {
#[default]
Chat,
Completion,
Embedding,
Rerank,
#[serde(alias = "image-generation", alias = "imageGeneration")]
ImageGen,
#[serde(alias = "image-edit")]
ImageEdit,
Video,
Audio,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq, Eq)]
#[ts(export)]
pub enum Modality {
Text,
Image,
Audio,
Video,
Pdf,
Embedding,
Score,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct ModelLimits {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub context: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub output: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub input: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct ModelModalities {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub input: Vec<Modality>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub output: Vec<Modality>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct ModelCapabilities {
#[serde(default)]
pub tool_call: bool,
#[serde(default, rename = "structured_output")]
pub structured_output: bool,
#[serde(default)]
pub temperature: bool,
#[serde(default)]
pub attachment: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq, Eq)]
#[ts(export)]
pub enum ReasoningMode {
Budget,
Effort,
Level,
Fixed,
Mixed,
Toggle,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq, Eq)]
#[ts(export)]
pub enum ReasoningVisibility {
Hidden,
Summary,
Full,
Mixed,
Omitted,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct ReasoningSpec {
#[serde(default)]
pub supported: bool,
#[serde(default)]
pub default_enabled: bool,
#[serde(default)]
pub mode: Option<ReasoningMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effort_default: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub effort_options: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub budget_min: Option<u64>,
#[serde(default)]
pub interleaved: bool,
#[serde(default)]
pub visibility: Option<ReasoningVisibility>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct ModelCost {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)]
#[ts(export)]
pub struct ModelSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default)]
pub r#type: ModelType,
#[serde(default)]
pub limits: ModelLimits,
#[serde(default)]
pub modalities: ModelModalities,
#[serde(default)]
pub capabilities: ModelCapabilities,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<ModelCost>,
#[serde(default)]
pub source: CatalogueSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_model_construction() {
let r = RuntimeModel {
id: "gpt-4o".into(),
owned_by: Some("openai".into()),
created: Some(1715367049),
};
assert_eq!(r.id, "gpt-4o");
assert_eq!(r.owned_by.as_deref(), Some("openai"));
}
#[test]
fn model_spec_with_limits_and_caps() {
let spec = ModelSpec {
limits: ModelLimits {
context: Some(128000),
output: Some(16384),
..Default::default()
},
capabilities: ModelCapabilities {
tool_call: true,
structured_output: true,
..Default::default()
},
source: CatalogueSource::Anya2a,
..Default::default()
};
assert_eq!(spec.limits.context, Some(128000));
assert!(spec.capabilities.tool_call);
}
#[test]
fn model_type_aliases_loose_deserialize() {
let json = r#"{"type":"image-generation"}"#;
let m: ModelSpec = serde_json::from_str(json).unwrap();
assert_eq!(m.r#type, ModelType::ImageGen);
let json = r#"{"type":"weird"}"#;
let m: ModelSpec = serde_json::from_str(json).unwrap();
assert_eq!(m.r#type, ModelType::Other);
}
#[test]
fn empty_json_yields_defaults() {
let m: ModelSpec = serde_json::from_str("{}").unwrap();
assert_eq!(m.r#type, ModelType::Chat);
assert!(!m.capabilities.tool_call);
assert!(m.reasoning.is_none());
}
}