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    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub execution_evidence: Option<RemoteExecutionEvidence>,
153}
154
155#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
156pub struct RemoteExecutionEvidence {
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub served_model: Option<String>,
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub provider_response_id: Option<String>,
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub provider_request_id: Option<String>,
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub reasoning_output_tokens: Option<u64>,
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub provider_finish_reason: Option<String>,
167}
168
169impl RemoteLlmResponse {
170    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
171        ensure_protocol_version(self.protocol_version)?;
172        require_non_empty("RemoteLlmResponse", "request_id", &self.request_id)?;
173        Ok(())
174    }
175}
176
177#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
178pub struct RemoteModelIntent {
179    pub model: String,
180    #[serde(default)]
181    pub variant: RemoteReasoningSelection,
182    /// Host-supplied capability metadata for the model (mirrors the core
183    /// `ModelCapability` contract).
184    #[serde(default, skip_serializing_if = "RemoteModelCapability::is_empty")]
185    pub capability: RemoteModelCapability,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub provider: Option<String>,
188    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
189    pub metadata: HashMap<String, String>,
190}
191
192/// Mirror of the core `ModelCapability`: host-supplied model capability
193/// metadata carried with the model intent so remote workers validate and
194/// encode effort exactly like a local runtime.
195#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
196pub struct RemoteModelCapability {
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub reasoning: Option<RemoteReasoningCapability>,
199}
200
201impl RemoteModelCapability {
202    pub fn is_empty(&self) -> bool {
203        self.reasoning.is_none()
204    }
205}
206
207/// Mirror of the core `ReasoningCapability`.
208#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
209pub struct RemoteReasoningCapability {
210    #[serde(default)]
211    pub efforts: Vec<String>,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub default_effort: Option<String>,
214    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
215    pub aliases: BTreeMap<String, String>,
216    #[serde(default)]
217    pub encoding: RemoteReasoningEncoding,
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub disable: Option<RemoteReasoningDisableEncoding>,
220    #[serde(default)]
221    pub mandatory: bool,
222}
223
224#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
225#[serde(rename_all = "snake_case")]
226pub enum RemoteReasoningSelection {
227    #[default]
228    ProviderDefault,
229    Disabled,
230    Effort(String),
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
234#[serde(rename_all = "snake_case")]
235pub enum RemoteReasoningDisableEncoding {
236    Native,
237    Omit,
238    Effort(String),
239    Budget(u32),
240    ToggleFalse,
241}
242
243/// Mirror of the core `ReasoningEncoding`.
244#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
245#[serde(rename_all = "snake_case")]
246pub enum RemoteReasoningEncoding {
247    #[default]
248    Effort,
249    Budget(BTreeMap<String, u32>),
250}
251
252impl RemoteModelIntent {
253    pub fn new(model: impl Into<String>) -> Self {
254        Self {
255            model: model.into(),
256            variant: RemoteReasoningSelection::ProviderDefault,
257            capability: RemoteModelCapability::default(),
258            provider: None,
259            metadata: HashMap::new(),
260        }
261    }
262
263    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
264        require_non_empty("RemoteModelIntent", "model", &self.model)
265    }
266}
267
268#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
269pub struct RemoteGenerationOptions {
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub output_token_cap: Option<u64>,
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub temperature: Option<String>,
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub top_p: Option<String>,
276    #[serde(default, skip_serializing_if = "Vec::is_empty")]
277    pub stop: Vec<String>,
278    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
279    pub provider_options: HashMap<String, String>,
280}
281
282impl RemoteGenerationOptions {
283    pub fn is_empty(&self) -> bool {
284        self.output_token_cap.is_none()
285            && self.temperature.is_none()
286            && self.top_p.is_none()
287            && self.stop.is_empty()
288            && self.provider_options.is_empty()
289    }
290
291    pub(crate) fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
292        if self.output_token_cap == Some(0) {
293            return Err(RemoteProtocolError::InvalidEnvelope {
294                type_name,
295                message: "generation.output_token_cap must be greater than zero".to_string(),
296            });
297        }
298        Ok(())
299    }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303pub struct RemoteLlmRequestScope {
304    pub session_id: String,
305    pub agent_frame_id: String,
306    pub request_id: String,
307}
308
309impl RemoteLlmRequestScope {
310    pub fn new(
311        session_id: impl Into<String>,
312        agent_frame_id: impl Into<String>,
313        request_id: impl Into<String>,
314    ) -> Self {
315        Self {
316            session_id: session_id.into(),
317            agent_frame_id: agent_frame_id.into(),
318            request_id: request_id.into(),
319        }
320    }
321
322    fn validate(&self) -> Result<(), RemoteProtocolError> {
323        require_non_empty("RemoteLlmRequestScope", "session_id", &self.session_id)?;
324        require_non_empty(
325            "RemoteLlmRequestScope",
326            "agent_frame_id",
327            &self.agent_frame_id,
328        )?;
329        require_non_empty("RemoteLlmRequestScope", "request_id", &self.request_id)?;
330        Ok(())
331    }
332}
333
334#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
335#[serde(rename_all = "snake_case")]
336pub enum RemoteLlmRole {
337    #[default]
338    User,
339    Assistant,
340    System,
341}
342
343#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
344pub struct RemoteLlmMessage {
345    pub role: RemoteLlmRole,
346    #[serde(default, skip_serializing_if = "Vec::is_empty")]
347    pub content: Vec<RemoteLlmContentBlock>,
348}
349
350impl RemoteLlmMessage {
351    fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
352        if self.content.is_empty() {
353            return Err(RemoteProtocolError::InvalidEnvelope {
354                type_name: "RemoteLlmMessage",
355                message: format!("message at index {index} must contain at least one block"),
356            });
357        }
358        for block in &self.content {
359            block.validate()?;
360        }
361        Ok(())
362    }
363}
364
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
366#[serde(tag = "type", rename_all = "snake_case")]
367pub enum RemoteLlmContentBlock {
368    Text {
369        text: String,
370        #[serde(default, skip_serializing_if = "Option::is_none")]
371        response_meta: Option<RemoteResponseTextMeta>,
372        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
373        cache_breakpoint: bool,
374    },
375    ImageAttachment {
376        attachment_index: usize,
377    },
378    ToolCall {
379        call_id: String,
380        tool_name: String,
381        input_json: String,
382        #[serde(default, skip_serializing_if = "Option::is_none")]
383        replay: Option<RemoteProviderReplayMeta>,
384    },
385    ToolResult {
386        call_id: String,
387        content: String,
388        #[serde(default, skip_serializing_if = "Option::is_none")]
389        tool_name: Option<String>,
390    },
391    Reasoning {
392        text: String,
393        #[serde(default, skip_serializing_if = "Option::is_none")]
394        replay: Option<RemoteProviderReasoningReplay>,
395    },
396}
397
398impl RemoteLlmContentBlock {
399    fn validate(&self) -> Result<(), RemoteProtocolError> {
400        match self {
401            Self::ToolCall {
402                call_id, tool_name, ..
403            } => {
404                require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
405                require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
406            }
407            Self::ToolResult { call_id, .. } => {
408                require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
409            }
410            Self::Text { .. } | Self::ImageAttachment { .. } | Self::Reasoning { .. } => Ok(()),
411        }
412    }
413}
414
415#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
416pub struct RemoteResponseTextMeta {
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub id: Option<String>,
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub status: Option<String>,
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub phase: Option<String>,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub provider_payload: Option<String>,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub origin_provider: Option<String>,
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub origin_model: Option<String>,
429}
430
431#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
432pub struct RemoteProviderReplayMeta {
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    pub item_id: Option<String>,
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub opaque: Option<String>,
437}
438
439#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
440pub struct RemoteProviderReasoningReplay {
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub item_id: Option<String>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub encrypted_content: Option<String>,
445    #[serde(default, skip_serializing_if = "Option::is_none")]
446    pub signature: Option<String>,
447    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
448    pub redacted: bool,
449    #[serde(default, skip_serializing_if = "Vec::is_empty")]
450    pub summary: Vec<String>,
451}
452
453#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
454pub struct RemoteLlmAttachment {
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub id: Option<String>,
457    pub mime: String,
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub data_base64: Option<String>,
460    #[serde(default, skip_serializing_if = "Option::is_none")]
461    pub reference: Option<RemoteAttachmentRef>,
462    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
463    pub metadata: HashMap<String, String>,
464}
465
466impl RemoteLlmAttachment {
467    fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
468        if self.mime.trim().is_empty() {
469            return Err(RemoteProtocolError::InvalidEnvelope {
470                type_name: "RemoteLlmAttachment",
471                message: format!("attachment at index {index} requires a non-empty mime"),
472            });
473        }
474        if let Some(reference) = &self.reference {
475            reference.validate()?;
476        }
477        Ok(())
478    }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
482pub struct RemoteAttachmentRef {
483    pub id: String,
484    pub mime: String,
485    pub byte_len: u64,
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub width: Option<u32>,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub height: Option<u32>,
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub label: Option<String>,
492    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
493    pub metadata: HashMap<String, String>,
494}
495
496impl RemoteAttachmentRef {
497    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
498        require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
499        require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
500    }
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
504pub struct RemoteLlmToolSpec {
505    pub name: String,
506    #[serde(default)]
507    pub description: String,
508    #[serde(default = "default_remote_input_schema")]
509    pub input_schema: RemoteSchemaContract,
510    #[serde(default)]
511    pub output_schema: RemoteSchemaContract,
512}
513
514impl RemoteLlmToolSpec {
515    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
516        require_non_empty("RemoteLlmToolSpec", "name", &self.name)
517    }
518}
519
520#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
521#[serde(rename_all = "snake_case")]
522pub enum RemoteLlmToolChoice {
523    #[default]
524    Auto,
525    None,
526    Required,
527}
528
529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
530#[serde(tag = "type", rename_all = "snake_case")]
531pub enum RemoteLlmOutputSpec {
532    JsonObject,
533    JsonSchema {
534        name: String,
535        schema: RemoteSchemaContract,
536        strict: bool,
537    },
538}
539
540impl RemoteLlmOutputSpec {
541    fn validate(&self) -> Result<(), RemoteProtocolError> {
542        match self {
543            Self::JsonObject => Ok(()),
544            Self::JsonSchema { name, .. } => {
545                require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
546            }
547        }
548    }
549}
550
551#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
552#[serde(tag = "type", rename_all = "snake_case")]
553pub enum RemoteLlmOutputPart {
554    Text {
555        text: String,
556        #[serde(default, skip_serializing_if = "Option::is_none")]
557        response_meta: Option<RemoteResponseTextMeta>,
558    },
559    Reasoning {
560        text: String,
561        #[serde(default, skip_serializing_if = "Option::is_none")]
562        replay: Option<RemoteProviderReasoningReplay>,
563    },
564    ToolCall {
565        call_id: String,
566        tool_name: String,
567        input_json: String,
568        #[serde(default, skip_serializing_if = "Option::is_none")]
569        replay: Option<RemoteProviderReplayMeta>,
570    },
571}
572
573#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
574#[serde(rename_all = "snake_case")]
575pub enum RemoteLlmTerminalReason {
576    Stop,
577    ToolUse,
578    OutputLimit,
579    ContextOverflow,
580    ContentFilter,
581    ProviderError,
582    Cancelled,
583    #[default]
584    Unknown,
585}
586
587/// Wire mirror of the core `ProviderFailureKind` classification.
588#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
589#[serde(rename_all = "snake_case")]
590pub enum RemoteProviderFailureKind {
591    Transport,
592    Timeout,
593    Http,
594    Stream,
595    Auth,
596    Validation,
597    Quota,
598    Unsupported,
599    #[default]
600    #[serde(other)]
601    Unknown,
602}
603
604#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
605pub struct RemoteProviderMetadata {
606    #[serde(default, skip_serializing_if = "Option::is_none")]
607    pub usage: Option<serde_json::Value>,
608    #[serde(default, skip_serializing_if = "Option::is_none")]
609    pub request_body: Option<String>,
610    #[serde(default, skip_serializing_if = "Option::is_none")]
611    pub http_summary: Option<String>,
612    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
613    pub data: HashMap<String, serde_json::Value>,
614}
615
616impl RemoteProviderMetadata {
617    pub fn is_empty(&self) -> bool {
618        self.usage.is_none()
619            && self.request_body.is_none()
620            && self.http_summary.is_none()
621            && self.data.is_empty()
622    }
623}
624
625#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
626pub struct RemoteDiagnostic {
627    pub kind: String,
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub code: Option<String>,
630    pub message: String,
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub data: Option<serde_json::Value>,
633}