models-dev 0.1.1

Simple Rust client for the models.dev API
Documentation
//! Data structures for the models.dev API schema.
//!
//! This module contains exact schema matches for the models.dev API response structures.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Top-level response structure from the models.dev API.
/// 
/// This is a map where keys are provider IDs and values are provider information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelsDevResponse {
    /// Provider information mapped by provider ID.
    #[serde(flatten)]
    pub providers: HashMap<String, Provider>,
}

impl ModelsDevResponse {
    /// Get the providers as a vector.
    pub fn providers_vec(&self) -> Vec<Provider> {
        self.providers.values().cloned().collect()
    }
}

/// Provider information from the models.dev API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Provider {
    /// The unique identifier for the provider.
    pub id: String,

    /// The display name of the provider.
    pub name: String,

    /// NPM package information for the provider.
    pub npm: String,

    /// Environment variables required for the provider.
    pub env: Vec<String>,

    /// Documentation information.
    pub doc: String,

    /// API configuration information.
    #[serde(default)]
    pub api: Option<String>,

    /// Available models for this provider.
    pub models: HashMap<String, Model>,
}

/// Model information from the models.dev API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Model {
    /// The unique identifier for the model.
    pub id: String,

    /// The display name of the model.
    pub name: String,

    /// Whether the model supports attachments.
    pub attachment: bool,

    /// Whether the model supports reasoning.
    pub reasoning: bool,

    /// Whether the model supports temperature.
    pub temperature: bool,

    /// Whether the model supports tool calls.
    pub tool_call: bool,

    /// Knowledge cutoff date.
    #[serde(default)]
    pub knowledge: Option<String>,

    /// Release date.
    #[serde(default)]
    pub release_date: Option<String>,

    /// Last updated date.
    #[serde(default)]
    pub last_updated: Option<String>,

    /// Supported modalities for this model.
    pub modalities: Modalities,

    /// Whether the model uses open weights.
    #[serde(default)]
    pub open_weights: bool,

    /// Cost information for using this model.
    #[serde(default)]
    pub cost: Option<ModelCost>,

    /// Limits for this model.
    pub limit: ModelLimit,
}

/// Cost information for a model.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCost {
    /// Cost per 1M input tokens.
    pub input: f64,

    /// Cost per 1M output tokens.
    pub output: f64,

    /// Cost per 1M cache read tokens (if supported).
    #[serde(default)]
    pub cache_read: Option<f64>,
}

/// Limits for a model.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelLimit {
    /// Maximum context window size in tokens.
    pub context: u32,

    /// Maximum output tokens per request.
    pub output: u32,
}

/// Supported modalities for a model.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Modalities {
    /// Supported input modalities.
    pub input: Vec<String>,

    /// Supported output modalities.
    pub output: Vec<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_models_dev_response_deserialization() {
        let json = r#"{
            "deepseek": {
                "id": "deepseek",
                "name": "DeepSeek",
                "npm": "@ai-sdk/openai-compatible",
                "env": ["DEEPSEEK_API_KEY"],
                "doc": "https://platform.deepseek.com/api-docs/pricing",
                "api": "https://api.deepseek.com",
                "models": {
                    "deepseek-chat": {
                        "id": "deepseek-chat",
                        "name": "DeepSeek Chat",
                        "attachment": true,
                        "reasoning": false,
                        "temperature": true,
                        "tool_call": true,
                        "modalities": {
                            "input": ["text"],
                            "output": ["text"]
                        },
                        "open_weights": false,
                        "cost": {
                            "input": 0.57,
                            "output": 1.68,
                            "cache_read": 0.07
                        },
                        "limit": {
                            "context": 128000,
                            "output": 8192
                        }
                    }
                }
            }
        }"#;

        let response: ModelsDevResponse = serde_json::from_str(json).unwrap();
        assert_eq!(response.providers.len(), 1);
        assert!(response.providers.contains_key("deepseek"));
        
        let provider = &response.providers["deepseek"];
        assert_eq!(provider.id, "deepseek");
        assert_eq!(provider.name, "DeepSeek");
        assert_eq!(provider.npm, "@ai-sdk/openai-compatible");
        assert_eq!(provider.env.len(), 1);
        assert_eq!(provider.env[0], "DEEPSEEK_API_KEY");
        
        let providers_vec = response.providers_vec();
        assert_eq!(providers_vec.len(), 1);
        assert_eq!(providers_vec[0].id, "deepseek");
    }

    #[test]
    fn test_model_cost_with_optional_fields() {
        let json = r#"{
            "input": 0.01,
            "output": 0.02,
            "cache_read": 0.005
        }"#;

        let cost: ModelCost = serde_json::from_str(json).unwrap();
        assert_eq!(cost.input, 0.01);
        assert_eq!(cost.output, 0.02);
        assert_eq!(cost.cache_read, Some(0.005));
    }

    #[test]
    fn test_model_cost_without_optional_fields() {
        let json = r#"{
            "input": 0.01,
            "output": 0.02
        }"#;

        let cost: ModelCost = serde_json::from_str(json).unwrap();
        assert_eq!(cost.input, 0.01);
        assert_eq!(cost.output, 0.02);
        assert_eq!(cost.cache_read, None);
    }

    #[test]
    fn test_modalities() {
        let json = r#"{
            "input": ["text", "image"],
            "output": ["text"]
        }"#;

        let modalities: Modalities = serde_json::from_str(json).unwrap();
        assert_eq!(modalities.input, vec!["text", "image"]);
        assert_eq!(modalities.output, vec!["text"]);
    }
}