use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
#[serde(rename = "type")]
pub provider_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
pub model: String,
pub base_url: Option<String>,
pub system_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
#[serde(default = "default_context_window")]
pub context_window: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_keep: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_history: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget: Option<u32>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub skip_tls_verify: bool,
#[serde(skip)]
pub ephemeral: bool,
}
impl ProviderConfig {
pub fn accepts_images(&self) -> bool {
crate::provider::model_name_suggests_vision(&self.model)
}
}
fn default_context_window() -> usize {
128000
}
pub fn default_context_window_for(provider_type: &str) -> usize {
match provider_type {
"ollama" => 8000,
_ => 128000,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_images_false_for_text_only_model() {
let toml_str = r#"
type = "openai"
model = "GLM-5.1"
api_key = "sk-test"
base_url = "https://api-ai.gitcode.com/v1"
"#;
let cfg: ProviderConfig = toml::from_str(toml_str).expect("parse");
assert!(!cfg.accepts_images());
}
#[test]
fn accepts_images_true_via_heuristic_on_known_vision_model() {
let toml_str = r#"
type = "claude"
model = "claude-sonnet-4-5"
api_key = "sk-test"
"#;
let cfg: ProviderConfig = toml::from_str(toml_str).expect("parse");
assert!(cfg.accepts_images());
}
#[test]
fn thinking_fields_default_to_none() {
let toml_str = r#"
type = "claude"
model = "claude-sonnet-4"
base_url = "https://api.anthropic.com"
context_window = 128000
"#;
let cfg: ProviderConfig = toml::from_str(toml_str).expect("parse");
assert!(cfg.thinking_enabled.is_none());
assert!(cfg.thinking_budget.is_none());
}
#[test]
fn thinking_fields_parse_correctly() {
let toml_str = r#"
type = "claude"
model = "claude-sonnet-4"
base_url = "https://api.anthropic.com"
context_window = 128000
thinking_enabled = true
thinking_budget = 20000
"#;
let cfg: ProviderConfig = toml::from_str(toml_str).expect("parse");
assert_eq!(cfg.thinking_enabled, Some(true));
assert_eq!(cfg.thinking_budget, Some(20000));
}
#[test]
fn skip_tls_verify_defaults_to_false() {
let toml_str = r#"
type = "openai"
model = "gpt-4o"
api_key = "sk-test"
"#;
let cfg: ProviderConfig = toml::from_str(toml_str).expect("parse");
assert!(
!cfg.skip_tls_verify,
"skip_tls_verify should default to false"
);
}
#[test]
fn skip_tls_verify_can_be_set_true() {
let toml_str = r#"
type = "openai"
model = "gpt-4o"
api_key = "sk-test"
base_url = "https://self-signed.example.com/v1"
skip_tls_verify = true
"#;
let cfg: ProviderConfig = toml::from_str(toml_str).expect("parse");
assert!(cfg.skip_tls_verify, "skip_tls_verify should be true");
}
#[test]
fn skip_tls_verify_not_serialized_when_false() {
let cfg = ProviderConfig {
provider_type: "openai".into(),
api_key: Some("sk-test".into()),
model: "gpt-4o".into(),
base_url: None,
system_prompt: None,
user_agent: None,
context_window: 128000,
max_tokens: None,
thinking_type: None,
thinking_keep: None,
reasoning_history: None,
reasoning_effort: None,
thinking_enabled: None,
thinking_budget: None,
skip_tls_verify: false,
ephemeral: false,
};
let serialized = toml::to_string(&cfg).expect("serialize");
assert!(
!serialized.contains("skip_tls_verify"),
"skip_tls_verify should not be serialized when false"
);
}
#[test]
fn skip_tls_verify_serialized_when_true() {
let cfg = ProviderConfig {
provider_type: "openai".into(),
api_key: Some("sk-test".into()),
model: "gpt-4o".into(),
base_url: Some("https://self-signed.example.com/v1".into()),
system_prompt: None,
user_agent: None,
context_window: 128000,
max_tokens: None,
thinking_type: None,
thinking_keep: None,
reasoning_history: None,
reasoning_effort: None,
thinking_enabled: None,
thinking_budget: None,
skip_tls_verify: true,
ephemeral: false,
};
let serialized = toml::to_string(&cfg).expect("serialize");
assert!(
serialized.contains("skip_tls_verify = true"),
"skip_tls_verify should be serialized when true"
);
}
}