Skip to main content

koda_core/
last_provider.rs

1//! Last-used provider recall (#693).
2//!
3//! Remembers the last provider/model/base-URL so Koda can auto-restore
4//! on next startup. Stored in SQLite via the [`crate::db`] KV store.
5//!
6//! This is **not** user configuration — Koda follows "customization over
7//! configuration" (see DESIGN.md §P1). The only persisted state is which
8//! provider the user last chose via `/model`.
9//!
10//! Renamed from `settings.rs` to avoid implying a user-editable config
11//! surface — koda has none.
12
13use crate::db::Database;
14use anyhow::Result;
15
16/// KV key for the last-used provider.
17const KV_KEY: &str = "setting:last_provider";
18
19/// Last-used provider configuration, restored on startup.
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct LastProvider {
22    /// Provider type name (e.g. `"Anthropic"`, `"Gemini"`).
23    pub provider_type: String,
24    /// API base URL.
25    pub base_url: String,
26    /// Model identifier.
27    pub model: String,
28}
29
30/// Load the last-used provider from the DB. Returns `None` if not set.
31pub async fn load_last_provider(db: &Database) -> Result<Option<LastProvider>> {
32    match db.kv_get(KV_KEY).await? {
33        Some(json) => Ok(serde_json::from_str(&json)?),
34        None => Ok(None),
35    }
36}
37
38/// Save the last-used provider to the DB.
39pub async fn save_last_provider(
40    db: &Database,
41    provider_type: &str,
42    base_url: &str,
43    model: &str,
44) -> Result<()> {
45    let lp = LastProvider {
46        provider_type: provider_type.to_string(),
47        base_url: base_url.to_string(),
48        model: model.to_string(),
49    };
50    let json = serde_json::to_string(&lp)?;
51    db.kv_set(KV_KEY, &json).await
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn test_last_provider_serde_round_trip() {
60        let lp = LastProvider {
61            provider_type: "Anthropic".into(),
62            base_url: "https://api.anthropic.com".into(),
63            model: "claude-opus-4-5".into(),
64        };
65        let json = serde_json::to_string(&lp).unwrap();
66        let parsed: LastProvider = serde_json::from_str(&json).unwrap();
67        assert_eq!(parsed.provider_type, "Anthropic");
68        assert_eq!(parsed.base_url, "https://api.anthropic.com");
69        assert_eq!(parsed.model, "claude-opus-4-5");
70    }
71
72    #[test]
73    fn test_last_provider_json_contains_expected_keys() {
74        let lp = LastProvider {
75            provider_type: "Gemini".into(),
76            base_url: "https://generativelanguage.googleapis.com".into(),
77            model: "gemini-2.5-pro".into(),
78        };
79        let json = serde_json::to_string(&lp).unwrap();
80        assert!(json.contains("provider_type"));
81        assert!(json.contains("base_url"));
82        assert!(json.contains("model"));
83        assert!(json.contains("Gemini"));
84    }
85
86    #[test]
87    fn test_last_provider_deserialize_invalid_json_returns_error() {
88        let result: Result<LastProvider, _> = serde_json::from_str("{not valid json}");
89        assert!(result.is_err());
90    }
91}