use serde::{Deserialize, Serialize};
use super::{DEFAULT_IMAGE_GENERATION_MODEL, DEFAULT_MODEL};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ThinkingLevel {
Minimal,
Low,
#[default]
Medium,
High,
}
impl ThinkingLevel {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
}
impl std::fmt::Display for ThinkingLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GenerationConfig {
#[serde(default)]
pub thinking_level: Option<ThinkingLevel>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ModelEntry {
pub name: String,
pub api_key: Option<String>,
#[serde(default)]
pub generation: GenerationConfig,
}
impl Default for ModelEntry {
fn default() -> Self {
default_model_entry()
}
}
impl std::fmt::Debug for ModelEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModelEntry")
.field("name", &self.name)
.field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
.field("generation", &self.generation)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
#[serde(default = "default_model_entry")]
pub default: ModelEntry,
#[serde(default = "default_image_model_entry")]
pub image_generation: ModelEntry,
}
pub(crate) fn default_model_entry() -> ModelEntry {
ModelEntry {
name: DEFAULT_MODEL.to_owned(),
api_key: None,
generation: GenerationConfig::default(),
}
}
pub(crate) fn default_image_model_entry() -> ModelEntry {
ModelEntry {
name: DEFAULT_IMAGE_GENERATION_MODEL.to_owned(),
api_key: None,
generation: GenerationConfig::default(),
}
}
impl Default for ModelConfig {
fn default() -> Self {
Self {
default: default_model_entry(),
image_generation: default_image_model_entry(),
}
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct GeminiConfig {
pub api_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default)]
pub models: ModelConfig,
}
impl std::fmt::Debug for GeminiConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GeminiConfig")
.field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
.field("base_url", &self.base_url)
.field("models", &self.models)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_thinking_level_serde() {
let level = ThinkingLevel::Minimal;
let json = serde_json::to_string(&level).unwrap();
assert_eq!(json, "\"minimal\"");
let parsed: ThinkingLevel = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ThinkingLevel::Minimal);
let level = ThinkingLevel::High;
let json = serde_json::to_string(&level).unwrap();
assert_eq!(json, "\"high\"");
assert_eq!(ThinkingLevel::Medium.as_str(), "medium");
}
#[test]
fn model_entry_serde_roundtrip() {
let entry = ModelEntry {
name: "gemini-3.5-flash".to_string(),
api_key: Some("mock_test_api_key_123".to_string()),
generation: GenerationConfig {
thinking_level: Some(ThinkingLevel::High),
},
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: ModelEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "gemini-3.5-flash");
assert_eq!(parsed.api_key.as_deref(), Some("mock_test_api_key_123"));
assert_eq!(parsed.generation.thinking_level, Some(ThinkingLevel::High));
}
#[test]
fn model_entry_minimal_serde() {
let json = r#"{"name":"flash"}"#;
let parsed: ModelEntry = serde_json::from_str(json).unwrap();
assert_eq!(parsed.name, "flash");
assert!(parsed.api_key.is_none());
assert!(parsed.generation.thinking_level.is_none());
}
#[test]
fn model_config_serde_roundtrip() {
let config = ModelConfig {
default: ModelEntry {
name: "gemini-3.5-flash".to_string(),
api_key: None,
generation: GenerationConfig::default(),
},
image_generation: ModelEntry {
name: "imagen-3".to_string(),
api_key: None,
generation: GenerationConfig::default(),
},
};
let json = serde_json::to_string(&config).unwrap();
let parsed: ModelConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.default.name, "gemini-3.5-flash");
assert_eq!(parsed.image_generation.name, "imagen-3");
}
#[test]
fn model_config_defaults() {
let config = ModelConfig::default();
assert_eq!(config.default.name, DEFAULT_MODEL);
assert_eq!(config.image_generation.name, DEFAULT_IMAGE_GENERATION_MODEL);
}
#[test]
fn gemini_config_serde_roundtrip() {
let config = GeminiConfig {
api_key: Some("global-key".to_string()),
base_url: None,
models: ModelConfig {
default: ModelEntry {
name: "gemini-3.5-flash".to_string(),
api_key: None,
generation: GenerationConfig::default(),
},
image_generation: default_image_model_entry(),
},
};
let json = serde_json::to_string(&config).unwrap();
let parsed: GeminiConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.api_key.as_deref(), Some("global-key"));
assert!(parsed.base_url.is_none());
assert_eq!(parsed.models.default.name, "gemini-3.5-flash");
}
#[test]
fn gemini_config_default() {
let config = GeminiConfig::default();
assert!(config.api_key.is_none());
assert_eq!(config.models.default.name, DEFAULT_MODEL);
assert_eq!(
config.models.image_generation.name,
DEFAULT_IMAGE_GENERATION_MODEL
);
}
#[test]
fn thinking_level_all_variants_python_str() {
assert_eq!(ThinkingLevel::Minimal.as_str(), "minimal");
assert_eq!(ThinkingLevel::Low.as_str(), "low");
assert_eq!(ThinkingLevel::Medium.as_str(), "medium");
assert_eq!(ThinkingLevel::High.as_str(), "high");
}
#[test]
fn thinking_level_all_variants_serde() {
for (variant, expected) in [
(ThinkingLevel::Minimal, "\"minimal\""),
(ThinkingLevel::Low, "\"low\""),
(ThinkingLevel::Medium, "\"medium\""),
(ThinkingLevel::High, "\"high\""),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected);
let parsed: ThinkingLevel = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, variant);
}
}
}