Skip to main content

llm_api/
service.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4pub const COMPLETE_TARGET: &str = "/llm/complete";
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
7#[serde(rename_all = "camelCase")]
8pub struct Image {
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub url: Option<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub base64: Option<String>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub mime_type: Option<String>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub created_at: Option<i64>,
17}
18
19impl Image {
20    #[must_use]
21    pub fn from_base64(base64: impl Into<String>, mime_type: impl Into<String>) -> Self {
22        Self {
23            base64: Some(base64.into()),
24            mime_type: Some(mime_type.into()),
25            ..Self::default()
26        }
27    }
28
29    #[must_use]
30    pub fn from_url(url: impl Into<String>) -> Self {
31        Self {
32            url: Some(url.into()),
33            ..Self::default()
34        }
35    }
36
37    #[must_use]
38    pub fn resolved_mime_type(&self) -> Option<String> {
39        let mime_type = self
40            .mime_type
41            .as_ref()
42            .map(|value| value.trim())
43            .filter(|value| !value.is_empty())?;
44        let normalized = mime_type.to_ascii_lowercase();
45        Some(match normalized.as_str() {
46            "image/jpg" => "image/jpeg".to_string(),
47            _ => normalized,
48        })
49    }
50
51    #[must_use]
52    pub fn is_empty(&self) -> bool {
53        self.url
54            .as_ref()
55            .is_none_or(|value| value.trim().is_empty())
56            && self
57                .base64
58                .as_ref()
59                .is_none_or(|value| value.trim().is_empty())
60    }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, Default)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct LlmGenerationOptions {
66    #[serde(default)]
67    pub temperature: Option<f32>,
68    #[serde(default)]
69    pub max_tokens: Option<u32>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, Default)]
73#[serde(rename_all = "camelCase", deny_unknown_fields)]
74pub struct LlmCompleteRequest {
75    #[serde(default)]
76    pub constraints: crate::ModelConstraints,
77    #[serde(default)]
78    pub use_case: Option<String>,
79    #[serde(default)]
80    pub mode: Option<String>,
81    pub messages: Vec<crate::Message>,
82    #[serde(default)]
83    pub tools: Option<Vec<crate::ToolDefinition>>,
84    #[serde(default)]
85    pub provider: Option<Value>,
86    #[serde(default)]
87    pub response_format: Option<Value>,
88    #[serde(default)]
89    pub options: LlmGenerationOptions,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93#[serde(rename_all = "camelCase")]
94pub struct LlmRouteInfo {
95    pub provider: String,
96    pub model: String,
97    pub mode: String,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101#[serde(rename_all = "camelCase")]
102pub struct LlmCompleteResponse {
103    pub message: crate::Message,
104    pub finish_reason: crate::FinishReason,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub usage: Option<crate::TokenUsage>,
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub route: Option<LlmRouteInfo>,
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn request_rejects_removed_top_level_generation_options() {
117        assert!(
118            serde_json::from_value::<LlmCompleteRequest>(serde_json::json!({
119                "messages": [],
120                "options": {},
121                "temperature": 0.2
122            }))
123            .is_err()
124        );
125    }
126
127    #[test]
128    fn response_includes_normalized_usage() {
129        let response = LlmCompleteResponse {
130            usage: Some(crate::TokenUsage {
131                input_tokens: 2,
132                output_tokens: 3,
133                ..crate::TokenUsage::default()
134            }),
135            ..LlmCompleteResponse::default()
136        };
137        assert_eq!(response.usage.unwrap().total_tokens(), 5);
138    }
139}