Skip to main content

yule_api/
types.rs

1use serde::{Deserialize, Serialize};
2
3// -- OpenAI compat types --
4
5#[derive(Debug, Deserialize)]
6pub struct ChatCompletionRequest {
7    pub model: String,
8    pub messages: Vec<ChatMessage>,
9    pub temperature: Option<f32>,
10    pub top_p: Option<f32>,
11    pub max_tokens: Option<u32>,
12    pub stream: Option<bool>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ChatMessage {
17    pub role: String,
18    pub content: String,
19}
20
21#[derive(Debug, Serialize)]
22pub struct ChatCompletionResponse {
23    pub id: String,
24    pub object: String,
25    pub created: u64,
26    pub model: String,
27    pub choices: Vec<ChatChoice>,
28    pub usage: Usage,
29}
30
31#[derive(Debug, Serialize)]
32pub struct ChatChoice {
33    pub index: u32,
34    pub message: ChatMessage,
35    pub finish_reason: String,
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct Usage {
40    pub prompt_tokens: u32,
41    pub completion_tokens: u32,
42    pub total_tokens: u32,
43}
44
45#[derive(Debug, Serialize)]
46pub struct ModelListResponse {
47    pub object: String,
48    pub data: Vec<OpenAiModelInfo>,
49}
50
51#[derive(Debug, Serialize)]
52pub struct OpenAiModelInfo {
53    pub id: String,
54    pub object: String,
55    pub owned_by: String,
56}
57
58// -- SSE streaming (OpenAI format) --
59
60#[derive(Debug, Serialize)]
61pub struct StreamChunk {
62    pub id: String,
63    pub object: String,
64    pub created: u64,
65    pub model: String,
66    pub choices: Vec<StreamChoice>,
67}
68
69#[derive(Debug, Serialize)]
70pub struct StreamChoice {
71    pub index: u32,
72    pub delta: StreamDelta,
73    pub finish_reason: Option<String>,
74}
75
76#[derive(Debug, Serialize)]
77pub struct StreamDelta {
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub role: Option<String>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub content: Option<String>,
82}
83
84// -- Yule-native types --
85
86#[derive(Debug, Deserialize)]
87pub struct YuleChatRequest {
88    pub messages: Vec<ChatMessage>,
89    pub max_tokens: Option<u32>,
90    pub temperature: Option<f32>,
91    pub top_p: Option<f32>,
92    pub stream: Option<bool>,
93}
94
95#[derive(Debug, Serialize)]
96pub struct YuleChatResponse {
97    pub id: String,
98    pub text: String,
99    pub finish_reason: String,
100    pub usage: Usage,
101    pub integrity: IntegrityInfo,
102    pub timing: TimingInfo,
103}
104
105#[derive(Debug, Clone, Serialize)]
106pub struct IntegrityInfo {
107    pub model_merkle_root: String,
108    pub model_verified: bool,
109    pub sandbox_active: bool,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub attestation_id: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub device_pubkey: Option<String>,
114}
115
116#[derive(Debug, Serialize)]
117pub struct TimingInfo {
118    pub prefill_ms: f64,
119    pub decode_ms: f64,
120    pub tokens_per_second: f64,
121}
122
123#[derive(Debug, Serialize)]
124pub struct HealthResponse {
125    pub status: String,
126    pub version: String,
127    pub uptime_seconds: u64,
128    pub model: Option<String>,
129    pub architecture: Option<String>,
130    pub sandbox: bool,
131}
132
133#[derive(Debug, Serialize)]
134pub struct YuleModelInfo {
135    pub name: Option<String>,
136    pub architecture: String,
137    pub parameters: String,
138    pub context_length: u32,
139    pub embedding_dim: u32,
140    pub layers: u32,
141    pub vocab_size: u32,
142    pub tensor_count: usize,
143    pub merkle_root: String,
144}
145
146#[derive(Debug, Deserialize)]
147pub struct TokenizeRequest {
148    pub text: String,
149}
150
151#[derive(Debug, Serialize)]
152pub struct TokenizeResponse {
153    pub tokens: Vec<u32>,
154    pub count: usize,
155}
156
157// -- Yule SSE stream event --
158
159#[derive(Debug, Serialize)]
160pub struct YuleStreamEvent {
161    #[serde(rename = "type")]
162    pub event_type: String,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub text: Option<String>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub usage: Option<Usage>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub integrity: Option<IntegrityInfo>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub timing: Option<TimingInfo>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub finish_reason: Option<String>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub error: Option<String>,
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn yule_chat_request_deserialize() {
183        let json = r#"{"messages":[{"role":"user","content":"hello"}],"max_tokens":100,"temperature":0.5}"#;
184        let req: YuleChatRequest = serde_json::from_str(json).unwrap();
185        assert_eq!(req.messages.len(), 1);
186        assert_eq!(req.messages[0].role, "user");
187        assert_eq!(req.max_tokens, Some(100));
188        assert_eq!(req.stream, None);
189    }
190
191    #[test]
192    fn yule_chat_request_defaults() {
193        let json = r#"{"messages":[{"role":"user","content":"hi"}]}"#;
194        let req: YuleChatRequest = serde_json::from_str(json).unwrap();
195        assert_eq!(req.max_tokens, None);
196        assert_eq!(req.temperature, None);
197        assert_eq!(req.top_p, None);
198        assert_eq!(req.stream, None);
199    }
200
201    #[test]
202    fn openai_request_deserialize() {
203        let json = r#"{"model":"gpt-4","messages":[{"role":"system","content":"you are helpful"},{"role":"user","content":"hi"}],"stream":true}"#;
204        let req: ChatCompletionRequest = serde_json::from_str(json).unwrap();
205        assert_eq!(req.model, "gpt-4");
206        assert_eq!(req.messages.len(), 2);
207        assert_eq!(req.stream, Some(true));
208    }
209
210    #[test]
211    fn yule_chat_response_serialize() {
212        let resp = YuleChatResponse {
213            id: "test-123".into(),
214            text: "hello world".into(),
215            finish_reason: "stop".into(),
216            usage: Usage { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
217            integrity: IntegrityInfo {
218                model_merkle_root: "abcdef".into(),
219                model_verified: true,
220                sandbox_active: true,
221                attestation_id: None,
222                device_pubkey: None,
223            },
224            timing: TimingInfo {
225                prefill_ms: 100.0,
226                decode_ms: 200.0,
227                tokens_per_second: 25.0,
228            },
229        };
230        let json = serde_json::to_string(&resp).unwrap();
231        assert!(json.contains("\"model_merkle_root\":\"abcdef\""));
232        assert!(json.contains("\"sandbox_active\":true"));
233        assert!(json.contains("\"tokens_per_second\":25.0"));
234    }
235
236    #[test]
237    fn stream_event_skips_none_fields() {
238        let event = YuleStreamEvent {
239            event_type: "token".into(),
240            text: Some("hi".into()),
241            usage: None,
242            integrity: None,
243            timing: None,
244            finish_reason: None,
245            error: None,
246        };
247        let json = serde_json::to_string(&event).unwrap();
248        assert!(json.contains("\"text\":\"hi\""));
249        assert!(!json.contains("usage"));
250        assert!(!json.contains("integrity"));
251        assert!(!json.contains("timing"));
252    }
253
254    #[test]
255    fn stream_delta_skips_none() {
256        let delta = StreamDelta { role: None, content: Some("word".into()) };
257        let json = serde_json::to_string(&delta).unwrap();
258        assert!(!json.contains("role"));
259        assert!(json.contains("\"content\":\"word\""));
260    }
261
262    #[test]
263    fn health_response_serialize() {
264        let resp = HealthResponse {
265            status: "healthy".into(),
266            version: "0.1.0".into(),
267            uptime_seconds: 42,
268            model: Some("llama".into()),
269            architecture: Some("LlamaForCausalLM".into()),
270            sandbox: false,
271        };
272        let json = serde_json::to_string(&resp).unwrap();
273        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
274        assert_eq!(parsed["status"], "healthy");
275        assert_eq!(parsed["uptime_seconds"], 42);
276        assert_eq!(parsed["sandbox"], false);
277    }
278
279    #[test]
280    fn tokenize_request_deserialize() {
281        let json = r#"{"text":"hello world"}"#;
282        let req: TokenizeRequest = serde_json::from_str(json).unwrap();
283        assert_eq!(req.text, "hello world");
284    }
285}