Skip to main content

lash_remote_protocol/
llm.rs

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