Skip to main content

lash_remote_protocol/protocol/
llm.rs

1#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2pub struct RemoteLlmRequest {
3    pub protocol_version: u32,
4    pub request_id: String,
5    pub model_intent: RemoteModelIntent,
6    #[serde(default, skip_serializing_if = "Vec::is_empty")]
7    pub messages: Vec<RemoteLlmMessage>,
8    #[serde(default, skip_serializing_if = "Vec::is_empty")]
9    pub attachments: Vec<RemoteLlmAttachment>,
10    #[serde(default, skip_serializing_if = "Vec::is_empty")]
11    pub tools: Vec<RemoteLlmToolSpec>,
12    #[serde(default)]
13    pub tool_choice: RemoteLlmToolChoice,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub output_spec: Option<RemoteLlmOutputSpec>,
16    #[serde(default, skip_serializing_if = "RemoteGenerationOptions::is_empty")]
17    pub generation: RemoteGenerationOptions,
18    #[serde(default, skip_serializing_if = "RemoteLlmRequestMetadata::is_empty")]
19    pub request_metadata: RemoteLlmRequestMetadata,
20    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
21    pub metadata: HashMap<String, serde_json::Value>,
22}
23
24impl RemoteLlmRequest {
25    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
26        ensure_protocol_version(self.protocol_version)?;
27        require_non_empty("RemoteLlmRequest", "request_id", &self.request_id)?;
28        self.model_intent.validate()?;
29        self.generation.validate("RemoteLlmRequest")?;
30        for (index, message) in self.messages.iter().enumerate() {
31            message.validate(index)?;
32        }
33        for (index, attachment) in self.attachments.iter().enumerate() {
34            attachment.validate(index)?;
35        }
36        for tool in &self.tools {
37            tool.validate()?;
38        }
39        if let Some(output_spec) = &self.output_spec {
40            output_spec.validate()?;
41        }
42        Ok(())
43    }
44}
45
46#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
47pub struct RemoteLlmResponse {
48    pub protocol_version: u32,
49    pub request_id: String,
50    #[serde(default)]
51    pub full_text: String,
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub output_parts: Vec<RemoteLlmOutputPart>,
54    #[serde(default)]
55    pub usage: RemoteUsage,
56    #[serde(default)]
57    pub terminal_reason: RemoteLlmTerminalReason,
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub diagnostics: Vec<RemoteDiagnostic>,
60    #[serde(default, skip_serializing_if = "RemoteProviderMetadata::is_empty")]
61    pub provider_metadata: RemoteProviderMetadata,
62}
63
64impl RemoteLlmResponse {
65    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
66        ensure_protocol_version(self.protocol_version)?;
67        require_non_empty("RemoteLlmResponse", "request_id", &self.request_id)?;
68        Ok(())
69    }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73pub struct RemoteModelIntent {
74    pub model: String,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub variant: Option<String>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub provider: Option<String>,
79    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
80    pub metadata: HashMap<String, String>,
81}
82
83impl RemoteModelIntent {
84    pub fn new(model: impl Into<String>) -> Self {
85        Self {
86            model: model.into(),
87            variant: None,
88            provider: None,
89            metadata: HashMap::new(),
90        }
91    }
92
93    fn validate(&self) -> Result<(), RemoteProtocolError> {
94        require_non_empty("RemoteModelIntent", "model", &self.model)
95    }
96}
97
98#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
99pub struct RemoteGenerationOptions {
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub output_token_cap: Option<u64>,
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub temperature: Option<String>,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub top_p: Option<String>,
106    #[serde(default, skip_serializing_if = "Vec::is_empty")]
107    pub stop: Vec<String>,
108    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
109    pub provider_options: HashMap<String, String>,
110}
111
112impl RemoteGenerationOptions {
113    pub fn is_empty(&self) -> bool {
114        self.output_token_cap.is_none()
115            && self.temperature.is_none()
116            && self.top_p.is_none()
117            && self.stop.is_empty()
118            && self.provider_options.is_empty()
119    }
120
121    fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
122        if self.output_token_cap == Some(0) {
123            return Err(RemoteProtocolError::InvalidEnvelope {
124                type_name,
125                message: "generation.output_token_cap must be greater than zero".to_string(),
126            });
127        }
128        Ok(())
129    }
130}
131
132#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133pub struct RemoteLlmRequestMetadata {
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub session_id: Option<String>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub idempotency_key: Option<String>,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub trace_id: Option<String>,
140}
141
142impl RemoteLlmRequestMetadata {
143    pub fn is_empty(&self) -> bool {
144        self.session_id.is_none() && self.idempotency_key.is_none() && self.trace_id.is_none()
145    }
146}
147
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
149#[serde(rename_all = "snake_case")]
150pub enum RemoteLlmRole {
151    #[default]
152    User,
153    Assistant,
154    System,
155}
156
157#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
158pub struct RemoteLlmMessage {
159    pub role: RemoteLlmRole,
160    #[serde(default, skip_serializing_if = "Vec::is_empty")]
161    pub content: Vec<RemoteLlmContentBlock>,
162}
163
164impl RemoteLlmMessage {
165    fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
166        if self.content.is_empty() {
167            return Err(RemoteProtocolError::InvalidEnvelope {
168                type_name: "RemoteLlmMessage",
169                message: format!("message at index {index} must contain at least one block"),
170            });
171        }
172        for block in &self.content {
173            block.validate()?;
174        }
175        Ok(())
176    }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
180#[serde(tag = "type", rename_all = "snake_case")]
181pub enum RemoteLlmContentBlock {
182    Text {
183        text: String,
184        #[serde(default, skip_serializing_if = "Option::is_none")]
185        response_meta: Option<RemoteResponseTextMeta>,
186        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
187        cache_breakpoint: bool,
188    },
189    ImageAttachment {
190        attachment_index: usize,
191    },
192    ToolCall {
193        call_id: String,
194        tool_name: String,
195        input_json: String,
196        #[serde(default, skip_serializing_if = "Option::is_none")]
197        replay: Option<RemoteProviderReplayMeta>,
198    },
199    ToolResult {
200        call_id: String,
201        content: String,
202        #[serde(default, skip_serializing_if = "Option::is_none")]
203        tool_name: Option<String>,
204    },
205    Reasoning {
206        text: String,
207        #[serde(default, skip_serializing_if = "Option::is_none")]
208        replay: Option<RemoteProviderReasoningReplay>,
209    },
210}
211
212impl RemoteLlmContentBlock {
213    fn validate(&self) -> Result<(), RemoteProtocolError> {
214        match self {
215            Self::ToolCall {
216                call_id, tool_name, ..
217            } => {
218                require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
219                require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
220            }
221            Self::ToolResult { call_id, .. } => {
222                require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
223            }
224            Self::Text { .. } | Self::ImageAttachment { .. } | Self::Reasoning { .. } => Ok(()),
225        }
226    }
227}
228
229#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
230pub struct RemoteResponseTextMeta {
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub id: Option<String>,
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub status: Option<String>,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub phase: Option<String>,
237}
238
239#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
240pub struct RemoteProviderReplayMeta {
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub item_id: Option<String>,
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub opaque: Option<String>,
245}
246
247#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
248pub struct RemoteProviderReasoningReplay {
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub item_id: Option<String>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub encrypted_content: Option<String>,
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub signature: Option<String>,
255    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
256    pub redacted: bool,
257    #[serde(default, skip_serializing_if = "Vec::is_empty")]
258    pub summary: Vec<String>,
259}
260
261#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
262pub struct RemoteLlmAttachment {
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub id: Option<String>,
265    pub mime: String,
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub data_base64: Option<String>,
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub reference: Option<RemoteAttachmentRef>,
270    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
271    pub metadata: HashMap<String, String>,
272}
273
274impl RemoteLlmAttachment {
275    fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
276        if self.mime.trim().is_empty() {
277            return Err(RemoteProtocolError::InvalidEnvelope {
278                type_name: "RemoteLlmAttachment",
279                message: format!("attachment at index {index} requires a non-empty mime"),
280            });
281        }
282        if let Some(reference) = &self.reference {
283            reference.validate()?;
284        }
285        Ok(())
286    }
287}
288
289#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
290pub struct RemoteAttachmentRef {
291    pub id: String,
292    pub mime: String,
293    pub byte_len: u64,
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub width: Option<u32>,
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub height: Option<u32>,
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub label: Option<String>,
300    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
301    pub metadata: HashMap<String, String>,
302}
303
304impl RemoteAttachmentRef {
305    fn validate(&self) -> Result<(), RemoteProtocolError> {
306        require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
307        require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
308    }
309}
310
311#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
312pub struct RemoteLlmToolSpec {
313    pub name: String,
314    #[serde(default)]
315    pub description: String,
316    #[serde(default = "default_input_schema")]
317    pub input_schema: serde_json::Value,
318    #[serde(default)]
319    pub output_schema: serde_json::Value,
320    #[serde(default, skip_serializing_if = "Vec::is_empty")]
321    pub input_schema_projections: Vec<RemoteSchemaProjectionOverride>,
322    #[serde(default, skip_serializing_if = "Vec::is_empty")]
323    pub output_schema_projections: Vec<RemoteSchemaProjectionOverride>,
324}
325
326impl RemoteLlmToolSpec {
327    fn validate(&self) -> Result<(), RemoteProtocolError> {
328        require_non_empty("RemoteLlmToolSpec", "name", &self.name)
329    }
330}
331
332#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
333#[serde(rename_all = "snake_case")]
334pub enum RemoteLlmToolChoice {
335    #[default]
336    Auto,
337    None,
338    Required,
339}
340
341#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
342#[serde(tag = "type", rename_all = "snake_case")]
343pub enum RemoteLlmOutputSpec {
344    JsonObject,
345    JsonSchema {
346        name: String,
347        schema: serde_json::Value,
348        strict: bool,
349    },
350}
351
352impl RemoteLlmOutputSpec {
353    fn validate(&self) -> Result<(), RemoteProtocolError> {
354        match self {
355            Self::JsonObject => Ok(()),
356            Self::JsonSchema { name, .. } => {
357                require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
358            }
359        }
360    }
361}
362
363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
364#[serde(tag = "type", rename_all = "snake_case")]
365pub enum RemoteLlmOutputPart {
366    Text {
367        text: String,
368        #[serde(default, skip_serializing_if = "Option::is_none")]
369        response_meta: Option<RemoteResponseTextMeta>,
370    },
371    Reasoning {
372        text: String,
373        #[serde(default, skip_serializing_if = "Option::is_none")]
374        replay: Option<RemoteProviderReasoningReplay>,
375    },
376    ToolCall {
377        call_id: String,
378        tool_name: String,
379        input_json: String,
380        #[serde(default, skip_serializing_if = "Option::is_none")]
381        replay: Option<RemoteProviderReplayMeta>,
382    },
383}
384
385#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
386#[serde(rename_all = "snake_case")]
387pub enum RemoteLlmTerminalReason {
388    Stop,
389    ToolUse,
390    OutputLimit,
391    ContextOverflow,
392    ContentFilter,
393    ProviderError,
394    Cancelled,
395    #[default]
396    Unknown,
397}
398
399#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
400pub struct RemoteProviderMetadata {
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub usage: Option<serde_json::Value>,
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub request_body: Option<String>,
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub http_summary: Option<String>,
407    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
408    pub data: HashMap<String, serde_json::Value>,
409}
410
411impl RemoteProviderMetadata {
412    pub fn is_empty(&self) -> bool {
413        self.usage.is_none()
414            && self.request_body.is_none()
415            && self.http_summary.is_none()
416            && self.data.is_empty()
417    }
418}
419
420#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
421pub struct RemoteDiagnostic {
422    pub kind: String,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub code: Option<String>,
425    pub message: String,
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub data: Option<serde_json::Value>,
428}