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<RemoteAttachmentSource>,
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    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub cache_control: Option<RemoteCacheControlDialect>,
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub stream_termination: Option<RemoteStreamTermination>,
203}
204
205impl RemoteModelCapability {
206    pub fn is_empty(&self) -> bool {
207        self.reasoning.is_none()
208            && self.cache_control.is_none()
209            && self.stream_termination.is_none()
210    }
211}
212
213/// Mirror of the core `CacheControlDialect`.
214#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
215#[serde(rename_all = "snake_case")]
216pub enum RemoteCacheControlDialect {
217    Anthropic,
218    Gemini,
219}
220
221/// Mirror of the core `StreamTermination`.
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
223#[serde(rename_all = "snake_case")]
224pub enum RemoteStreamTermination {
225    RequireTerminalEvidence,
226    EofTolerated,
227}
228
229/// Mirror of the core `ReasoningCapability`.
230#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231pub struct RemoteReasoningCapability {
232    #[serde(default)]
233    pub efforts: Vec<String>,
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub default_effort: Option<String>,
236    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
237    pub aliases: BTreeMap<String, String>,
238    #[serde(default)]
239    pub encoding: RemoteReasoningEncoding,
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub disable: Option<RemoteReasoningDisableEncoding>,
242    #[serde(default)]
243    pub mandatory: bool,
244}
245
246#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
247#[serde(rename_all = "snake_case")]
248pub enum RemoteReasoningSelection {
249    #[default]
250    ProviderDefault,
251    Disabled,
252    Effort(String),
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
256#[serde(rename_all = "snake_case")]
257pub enum RemoteReasoningDisableEncoding {
258    Native,
259    Omit,
260    Effort(String),
261    Budget(u32),
262    ToggleFalse,
263}
264
265/// Mirror of the core `ReasoningEncoding`.
266#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
267#[serde(rename_all = "snake_case")]
268pub enum RemoteReasoningEncoding {
269    #[default]
270    Effort,
271    Budget(BTreeMap<String, u32>),
272}
273
274impl RemoteModelIntent {
275    pub fn new(model: impl Into<String>) -> Self {
276        Self {
277            model: model.into(),
278            variant: RemoteReasoningSelection::ProviderDefault,
279            capability: RemoteModelCapability::default(),
280            provider: None,
281            metadata: HashMap::new(),
282        }
283    }
284
285    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
286        require_non_empty("RemoteModelIntent", "model", &self.model)
287    }
288}
289
290#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
291pub struct RemoteGenerationOptions {
292    #[serde(default, skip_serializing_if = "Option::is_none")]
293    pub output_token_cap: Option<u64>,
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub temperature: Option<String>,
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub top_p: Option<String>,
298    #[serde(default, skip_serializing_if = "Vec::is_empty")]
299    pub stop: Vec<String>,
300    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
301    pub provider_options: HashMap<String, String>,
302}
303
304impl RemoteGenerationOptions {
305    pub fn is_empty(&self) -> bool {
306        self.output_token_cap.is_none()
307            && self.temperature.is_none()
308            && self.top_p.is_none()
309            && self.stop.is_empty()
310            && self.provider_options.is_empty()
311    }
312
313    pub(crate) fn validate(&self, type_name: &'static str) -> Result<(), RemoteProtocolError> {
314        if self.output_token_cap == Some(0) {
315            return Err(RemoteProtocolError::InvalidEnvelope {
316                type_name,
317                message: "generation.output_token_cap must be greater than zero".to_string(),
318            });
319        }
320        Ok(())
321    }
322}
323
324#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
325pub struct RemoteLlmRequestScope {
326    pub session_id: String,
327    pub agent_frame_id: String,
328    pub request_id: String,
329}
330
331impl RemoteLlmRequestScope {
332    pub fn new(
333        session_id: impl Into<String>,
334        agent_frame_id: impl Into<String>,
335        request_id: impl Into<String>,
336    ) -> Self {
337        Self {
338            session_id: session_id.into(),
339            agent_frame_id: agent_frame_id.into(),
340            request_id: request_id.into(),
341        }
342    }
343
344    fn validate(&self) -> Result<(), RemoteProtocolError> {
345        require_non_empty("RemoteLlmRequestScope", "session_id", &self.session_id)?;
346        require_non_empty(
347            "RemoteLlmRequestScope",
348            "agent_frame_id",
349            &self.agent_frame_id,
350        )?;
351        require_non_empty("RemoteLlmRequestScope", "request_id", &self.request_id)?;
352        Ok(())
353    }
354}
355
356#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
357#[serde(rename_all = "snake_case")]
358pub enum RemoteLlmRole {
359    #[default]
360    User,
361    Assistant,
362    System,
363}
364
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
366pub struct RemoteLlmMessage {
367    pub role: RemoteLlmRole,
368    #[serde(default, skip_serializing_if = "Vec::is_empty")]
369    pub content: Vec<RemoteLlmContentBlock>,
370}
371
372impl RemoteLlmMessage {
373    fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
374        if self.content.is_empty() {
375            return Err(RemoteProtocolError::InvalidEnvelope {
376                type_name: "RemoteLlmMessage",
377                message: format!("message at index {index} must contain at least one block"),
378            });
379        }
380        for block in &self.content {
381            block.validate()?;
382        }
383        Ok(())
384    }
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
388#[serde(tag = "type", rename_all = "snake_case")]
389pub enum RemoteLlmContentBlock {
390    Text {
391        text: String,
392        #[serde(default, skip_serializing_if = "Option::is_none")]
393        response_meta: Option<RemoteResponseTextMeta>,
394        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
395        cache_breakpoint: bool,
396    },
397    Attachment {
398        attachment_index: usize,
399    },
400    ToolCall {
401        call_id: String,
402        tool_name: String,
403        input_json: String,
404        #[serde(default, skip_serializing_if = "Option::is_none")]
405        replay: Option<RemoteProviderReplayMeta>,
406    },
407    ToolResult {
408        call_id: String,
409        content: String,
410        #[serde(default, skip_serializing_if = "Option::is_none")]
411        tool_name: Option<String>,
412    },
413    Reasoning {
414        text: String,
415        #[serde(default, skip_serializing_if = "Option::is_none")]
416        replay: Option<RemoteProviderReasoningReplay>,
417    },
418}
419
420impl RemoteLlmContentBlock {
421    fn validate(&self) -> Result<(), RemoteProtocolError> {
422        match self {
423            Self::ToolCall {
424                call_id, tool_name, ..
425            } => {
426                require_non_empty("RemoteLlmContentBlock::ToolCall", "call_id", call_id)?;
427                require_non_empty("RemoteLlmContentBlock::ToolCall", "tool_name", tool_name)
428            }
429            Self::ToolResult { call_id, .. } => {
430                require_non_empty("RemoteLlmContentBlock::ToolResult", "call_id", call_id)
431            }
432            Self::Text { .. } | Self::Attachment { .. } | Self::Reasoning { .. } => Ok(()),
433        }
434    }
435}
436
437#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
438pub struct RemoteResponseTextMeta {
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub id: Option<String>,
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub status: Option<String>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub phase: Option<String>,
445    #[serde(default, skip_serializing_if = "Option::is_none")]
446    pub provider_payload: Option<String>,
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub origin_provider: Option<String>,
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub origin_model: Option<String>,
451}
452
453#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
454pub struct RemoteProviderReplayMeta {
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub item_id: Option<String>,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub opaque: Option<String>,
459}
460
461#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
462pub struct RemoteProviderReasoningReplay {
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub item_id: Option<String>,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub encrypted_content: Option<String>,
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub signature: Option<String>,
469    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
470    pub redacted: bool,
471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
472    pub summary: Vec<String>,
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
476#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
477pub enum RemoteAttachmentSource {
478    Inline {
479        media_type: String,
480        data_base64: String,
481    },
482    Stored {
483        attachment_ref: RemoteAttachmentRef,
484    },
485    ExternalUrl {
486        media_type: String,
487        url: String,
488    },
489    ProviderFile {
490        provider_scope: RemoteProviderFileScope,
491        id: String,
492    },
493}
494
495impl RemoteAttachmentSource {
496    pub(crate) fn validate(&self, _index: usize) -> Result<(), RemoteProtocolError> {
497        match self {
498            Self::Inline {
499                media_type,
500                data_base64,
501            } => {
502                require_non_empty("RemoteAttachmentSource::Inline", "media_type", media_type)?;
503                validate_media_type("RemoteAttachmentSource::Inline", media_type)?;
504                require_non_empty("RemoteAttachmentSource::Inline", "data_base64", data_base64)
505            }
506            Self::Stored { attachment_ref } => attachment_ref.validate(),
507            Self::ExternalUrl { media_type, url } => {
508                require_non_empty(
509                    "RemoteAttachmentSource::ExternalUrl",
510                    "media_type",
511                    media_type,
512                )?;
513                validate_media_type("RemoteAttachmentSource::ExternalUrl", media_type)?;
514                require_non_empty("RemoteAttachmentSource::ExternalUrl", "url", url)
515            }
516            Self::ProviderFile { provider_scope, id } => {
517                provider_scope.validate()?;
518                require_non_empty("RemoteAttachmentSource::ProviderFile", "id", id)
519            }
520        }
521    }
522}
523
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
525pub struct RemoteProviderFileScope {
526    pub provider: String,
527    pub credential_scope: String,
528}
529
530impl RemoteProviderFileScope {
531    fn validate(&self) -> Result<(), RemoteProtocolError> {
532        require_non_empty("RemoteProviderFileScope", "provider", &self.provider)?;
533        require_non_empty(
534            "RemoteProviderFileScope",
535            "credential_scope",
536            &self.credential_scope,
537        )
538    }
539}
540
541#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
542pub struct RemoteAttachmentRef {
543    pub id: String,
544    pub media_type: String,
545    pub byte_len: u64,
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub type_metadata: Option<RemoteAttachmentTypeMetadata>,
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub label: Option<String>,
550}
551
552impl RemoteAttachmentRef {
553    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
554        require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
555        require_non_empty("RemoteAttachmentRef", "media_type", &self.media_type)?;
556        validate_media_type("RemoteAttachmentRef", &self.media_type)
557    }
558}
559
560fn validate_media_type(type_name: &'static str, value: &str) -> Result<(), RemoteProtocolError> {
561    let mut pieces = value.split('/');
562    let type_token = pieces.next().unwrap_or_default();
563    let subtype_token = pieces.next().unwrap_or_default();
564    if pieces.next().is_some()
565        || !is_media_type_token(type_token)
566        || !is_media_type_token(subtype_token)
567    {
568        return Err(RemoteProtocolError::InvalidEnvelope {
569            type_name,
570            message: format!("media_type `{value}` must be a syntactically valid type/subtype"),
571        });
572    }
573    Ok(())
574}
575
576fn is_media_type_token(value: &str) -> bool {
577    !value.is_empty()
578        && value.bytes().all(|byte| {
579            byte.is_ascii_alphanumeric()
580                || matches!(
581                    byte,
582                    b'!' | b'#'
583                        | b'$'
584                        | b'%'
585                        | b'&'
586                        | b'\''
587                        | b'*'
588                        | b'+'
589                        | b'-'
590                        | b'.'
591                        | b'^'
592                        | b'_'
593                        | b'`'
594                        | b'|'
595                        | b'~'
596                )
597        })
598}
599
600#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
601#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
602pub enum RemoteAttachmentTypeMetadata {
603    Image {
604        #[serde(default, skip_serializing_if = "Option::is_none")]
605        width: Option<u32>,
606        #[serde(default, skip_serializing_if = "Option::is_none")]
607        height: Option<u32>,
608    },
609}
610
611#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
612pub struct RemoteLlmToolSpec {
613    pub name: String,
614    #[serde(default)]
615    pub description: String,
616    #[serde(default = "default_remote_input_schema")]
617    pub input_schema: RemoteSchemaContract,
618    #[serde(default)]
619    pub output_schema: RemoteSchemaContract,
620}
621
622impl RemoteLlmToolSpec {
623    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
624        require_non_empty("RemoteLlmToolSpec", "name", &self.name)
625    }
626}
627
628#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
629#[serde(rename_all = "snake_case")]
630pub enum RemoteLlmToolChoice {
631    #[default]
632    Auto,
633    None,
634    Required,
635}
636
637#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
638#[serde(tag = "type", rename_all = "snake_case")]
639pub enum RemoteLlmOutputSpec {
640    JsonObject,
641    JsonSchema {
642        name: String,
643        schema: RemoteSchemaContract,
644        strict: bool,
645    },
646}
647
648impl RemoteLlmOutputSpec {
649    fn validate(&self) -> Result<(), RemoteProtocolError> {
650        match self {
651            Self::JsonObject => Ok(()),
652            Self::JsonSchema { name, .. } => {
653                require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
654            }
655        }
656    }
657}
658
659#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
660#[serde(tag = "type", rename_all = "snake_case")]
661pub enum RemoteLlmOutputPart {
662    Text {
663        text: String,
664        #[serde(default, skip_serializing_if = "Option::is_none")]
665        response_meta: Option<RemoteResponseTextMeta>,
666    },
667    Reasoning {
668        text: String,
669        #[serde(default, skip_serializing_if = "Option::is_none")]
670        replay: Option<RemoteProviderReasoningReplay>,
671    },
672    ToolCall {
673        call_id: String,
674        tool_name: String,
675        input_json: String,
676        #[serde(default, skip_serializing_if = "Option::is_none")]
677        replay: Option<RemoteProviderReplayMeta>,
678    },
679}
680
681#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
682#[serde(rename_all = "snake_case")]
683pub enum RemoteLlmTerminalReason {
684    Stop,
685    ToolUse,
686    OutputLimit,
687    ContextOverflow,
688    ContentFilter,
689    ProviderError,
690    Cancelled,
691    #[default]
692    Unknown,
693}
694
695/// Wire mirror of the core `ProviderFailureKind` classification.
696#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
697#[serde(rename_all = "snake_case")]
698pub enum RemoteProviderFailureKind {
699    Transport,
700    Timeout,
701    Http,
702    Stream,
703    Auth,
704    Validation,
705    Quota,
706    Unsupported,
707    #[default]
708    #[serde(other)]
709    Unknown,
710}
711
712#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
713pub struct RemoteProviderMetadata {
714    #[serde(default, skip_serializing_if = "Option::is_none")]
715    pub usage: Option<serde_json::Value>,
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub request_body: Option<String>,
718    #[serde(default, skip_serializing_if = "Option::is_none")]
719    pub http_summary: Option<String>,
720    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
721    pub data: BTreeMap<String, serde_json::Value>,
722}
723
724impl RemoteProviderMetadata {
725    pub fn is_empty(&self) -> bool {
726        self.usage.is_none()
727            && self.request_body.is_none()
728            && self.http_summary.is_none()
729            && self.data.is_empty()
730    }
731}
732
733#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
734pub struct RemoteDiagnostic {
735    pub kind: String,
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub code: Option<String>,
738    pub message: String,
739    #[serde(default, skip_serializing_if = "Option::is_none")]
740    pub data: Option<serde_json::Value>,
741}