Skip to main content

lash_remote_protocol/protocol/
llm.rs

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