use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderListResponse {
#[serde(default)]
pub all: Vec<Provider>,
#[serde(default)]
pub default: HashMap<String, String>,
#[serde(default)]
pub connected: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum ProviderSource {
Env,
Config,
Custom,
Api,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Provider {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<ProviderSource>,
#[serde(default)]
pub env: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(default)]
pub options: HashMap<String, serde_json::Value>,
#[serde(default)]
pub models: HashMap<String, Model>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Model {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<ModelApi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<ModelCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<ModelCost>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<ModelLimit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<ModelStatus>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub release_date: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub variants: HashMap<String, serde_json::Value>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelApi {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
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>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelLimit {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<u64>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum ModelStatus {
Available,
Deprecated,
Beta,
Preview,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelCapabilities {
#[serde(default)]
pub tool_use: bool,
#[serde(default)]
pub vision: bool,
#[serde(default)]
pub thinking: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interleaved: Option<InterleavedCapability>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InterleavedCapability {
Bool(bool),
Config(InterleavedConfig),
Unknown(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InterleavedConfig {
pub field: InterleavedField,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum InterleavedField {
ReasoningContent,
ReasoningDetails,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderAuth {
pub provider_id: String,
pub method: AuthMethod,
#[serde(default)]
pub configured: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum AuthMethod {
ApiKey {
#[serde(default, skip_serializing_if = "Option::is_none")]
env_var: Option<String>,
},
Oauth {
#[serde(default, skip_serializing_if = "Option::is_none")]
authorize_url: Option<String>,
},
None,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetAuthRequest {
pub key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OAuthAuthorizeResponse {
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OAuthCallbackRequest {
pub code: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_minimal() {
let json = r#"{"id": "claude-3"}"#;
let model: Model = serde_json::from_str(json).unwrap();
assert_eq!(model.id, "claude-3");
assert!(model.name.is_none());
assert!(model.capabilities.is_none());
}
#[test]
fn test_model_full() {
let json = r#"{
"id": "claude-3",
"name": "Claude 3",
"api": {"endpoint": "https://api.example.com"},
"capabilities": {"toolUse": true, "vision": true, "thinking": false},
"cost": {"input": 3.0, "output": 15.0},
"limit": {"context": 200000, "output": 4096},
"status": "available",
"releaseDate": "2024-01-01",
"headers": {"X-Custom": "value"}
}"#;
let model: Model = serde_json::from_str(json).unwrap();
assert_eq!(model.id, "claude-3");
assert_eq!(model.name, Some("Claude 3".to_string()));
assert!(model.api.is_some());
assert!(model.capabilities.is_some());
let caps = model.capabilities.unwrap();
assert!(caps.tool_use);
assert!(caps.vision);
assert!(!caps.thinking);
assert!(model.cost.is_some());
let cost = model.cost.unwrap();
assert_eq!(cost.input, Some(3.0));
assert_eq!(cost.output, Some(15.0));
assert!(model.limit.is_some());
assert_eq!(model.limit.unwrap().context, Some(200_000));
assert_eq!(model.status, Some(ModelStatus::Available));
assert_eq!(model.release_date, Some("2024-01-01".to_string()));
assert_eq!(model.headers.get("X-Custom"), Some(&"value".to_string()));
}
#[test]
fn test_model_status_variants() {
assert_eq!(
serde_json::from_str::<ModelStatus>(r#""available""#).unwrap(),
ModelStatus::Available
);
assert_eq!(
serde_json::from_str::<ModelStatus>(r#""deprecated""#).unwrap(),
ModelStatus::Deprecated
);
assert_eq!(
serde_json::from_str::<ModelStatus>(r#""beta""#).unwrap(),
ModelStatus::Beta
);
assert_eq!(
serde_json::from_str::<ModelStatus>(r#""preview""#).unwrap(),
ModelStatus::Preview
);
assert_eq!(
serde_json::from_str::<ModelStatus>(r#""future-status""#).unwrap(),
ModelStatus::Unknown
);
}
#[test]
fn test_interleaved_capability_bool() {
let json = r"true";
let cap: InterleavedCapability = serde_json::from_str(json).unwrap();
assert!(matches!(cap, InterleavedCapability::Bool(true)));
}
#[test]
fn test_interleaved_capability_config() {
let json = r#"{"field": "reasoning_content"}"#;
let cap: InterleavedCapability = serde_json::from_str(json).unwrap();
if let InterleavedCapability::Config(config) = cap {
assert_eq!(config.field, InterleavedField::ReasoningContent);
} else {
panic!("Expected InterleavedCapability::Config");
}
}
#[test]
fn test_interleaved_capability_unknown_config() {
let json = r#"{"field": "future_field"}"#;
let cap: InterleavedCapability = serde_json::from_str(json).unwrap();
if let InterleavedCapability::Config(config) = cap {
assert_eq!(config.field, InterleavedField::Unknown);
} else {
panic!("Expected InterleavedCapability::Config");
}
}
#[test]
fn test_model_capabilities_with_interleaved() {
let json = r#"{
"toolUse": true,
"vision": false,
"thinking": true,
"interleaved": {"field": "reasoning_details"}
}"#;
let caps: ModelCapabilities = serde_json::from_str(json).unwrap();
assert!(caps.tool_use);
assert!(!caps.vision);
assert!(caps.thinking);
assert!(caps.interleaved.is_some());
if let Some(InterleavedCapability::Config(config)) = caps.interleaved {
assert_eq!(config.field, InterleavedField::ReasoningDetails);
} else {
panic!("Expected InterleavedCapability::Config");
}
}
#[test]
fn test_provider_source_variants() {
assert_eq!(
serde_json::from_str::<ProviderSource>(r#""env""#).unwrap(),
ProviderSource::Env
);
assert_eq!(
serde_json::from_str::<ProviderSource>(r#""config""#).unwrap(),
ProviderSource::Config
);
assert_eq!(
serde_json::from_str::<ProviderSource>(r#""future""#).unwrap(),
ProviderSource::Unknown
);
}
}