Skip to main content

spn_client/
protocol.rs

1//! Protocol types for daemon communication.
2//!
3//! The protocol uses length-prefixed JSON over Unix sockets.
4//!
5//! ## Wire Format
6//!
7//! ```text
8//! [4 bytes: message length (big-endian u32)][JSON payload]
9//! ```
10//!
11//! ## Example
12//!
13//! Request:
14//! ```json
15//! { "cmd": "GET_SECRET", "provider": "anthropic" }
16//! ```
17//!
18//! Response:
19//! ```json
20//! { "ok": true, "secret": "sk-ant-..." }
21//! ```
22
23use serde::{Deserialize, Serialize};
24use spn_core::{LoadConfig, ModelInfo, RunningModel};
25
26/// Request sent to the daemon.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "cmd")]
29pub enum Request {
30    /// Ping the daemon to check it's alive.
31    #[serde(rename = "PING")]
32    Ping,
33
34    /// Get a secret for a provider.
35    #[serde(rename = "GET_SECRET")]
36    GetSecret { provider: String },
37
38    /// Check if a secret exists.
39    #[serde(rename = "HAS_SECRET")]
40    HasSecret { provider: String },
41
42    /// List all available providers.
43    #[serde(rename = "LIST_PROVIDERS")]
44    ListProviders,
45
46    // ==================== Model Commands ====================
47
48    /// List all installed models.
49    #[serde(rename = "MODEL_LIST")]
50    ModelList,
51
52    /// Pull/download a model.
53    #[serde(rename = "MODEL_PULL")]
54    ModelPull { name: String },
55
56    /// Load a model into memory.
57    #[serde(rename = "MODEL_LOAD")]
58    ModelLoad {
59        name: String,
60        #[serde(default)]
61        config: Option<LoadConfig>,
62    },
63
64    /// Unload a model from memory.
65    #[serde(rename = "MODEL_UNLOAD")]
66    ModelUnload { name: String },
67
68    /// Get status of running models.
69    #[serde(rename = "MODEL_STATUS")]
70    ModelStatus,
71
72    /// Delete a model.
73    #[serde(rename = "MODEL_DELETE")]
74    ModelDelete { name: String },
75}
76
77/// Response from the daemon.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(untagged)]
80pub enum Response {
81    /// Successful ping response.
82    Pong { version: String },
83
84    /// Secret value response.
85    Secret { value: String },
86
87    /// Secret existence check response.
88    Exists { exists: bool },
89
90    /// Provider list response.
91    Providers { providers: Vec<String> },
92
93    // ==================== Model Responses ====================
94
95    /// List of installed models.
96    Models { models: Vec<ModelInfo> },
97
98    /// List of currently running/loaded models.
99    RunningModels { running: Vec<RunningModel> },
100
101    /// Generic success response.
102    Success { success: bool },
103
104    /// Error response.
105    Error { message: String },
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_request_serialization() {
114        let ping = Request::Ping;
115        let json = serde_json::to_string(&ping).unwrap();
116        assert_eq!(json, r#"{"cmd":"PING"}"#);
117
118        let get_secret = Request::GetSecret {
119            provider: "anthropic".to_string(),
120        };
121        let json = serde_json::to_string(&get_secret).unwrap();
122        assert_eq!(json, r#"{"cmd":"GET_SECRET","provider":"anthropic"}"#);
123
124        let has_secret = Request::HasSecret {
125            provider: "openai".to_string(),
126        };
127        let json = serde_json::to_string(&has_secret).unwrap();
128        assert_eq!(json, r#"{"cmd":"HAS_SECRET","provider":"openai"}"#);
129
130        let list = Request::ListProviders;
131        let json = serde_json::to_string(&list).unwrap();
132        assert_eq!(json, r#"{"cmd":"LIST_PROVIDERS"}"#);
133    }
134
135    #[test]
136    fn test_response_deserialization() {
137        // Pong
138        let json = r#"{"version":"0.9.0"}"#;
139        let response: Response = serde_json::from_str(json).unwrap();
140        assert!(matches!(response, Response::Pong { version } if version == "0.9.0"));
141
142        // Secret
143        let json = r#"{"value":"sk-test-123"}"#;
144        let response: Response = serde_json::from_str(json).unwrap();
145        assert!(matches!(response, Response::Secret { value } if value == "sk-test-123"));
146
147        // Exists
148        let json = r#"{"exists":true}"#;
149        let response: Response = serde_json::from_str(json).unwrap();
150        assert!(matches!(response, Response::Exists { exists } if exists));
151
152        // Providers
153        let json = r#"{"providers":["anthropic","openai"]}"#;
154        let response: Response = serde_json::from_str(json).unwrap();
155        assert!(
156            matches!(response, Response::Providers { providers } if providers == vec!["anthropic", "openai"])
157        );
158
159        // Error
160        let json = r#"{"message":"Not found"}"#;
161        let response: Response = serde_json::from_str(json).unwrap();
162        assert!(matches!(response, Response::Error { message } if message == "Not found"));
163    }
164}