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    #[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    ImageAttachment {
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::ImageAttachment { .. } | 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)]
476pub struct RemoteLlmAttachment {
477    #[serde(default, skip_serializing_if = "Option::is_none")]
478    pub id: Option<String>,
479    pub mime: String,
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub data_base64: Option<String>,
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub reference: Option<RemoteAttachmentRef>,
484    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
485    pub metadata: HashMap<String, String>,
486}
487
488impl RemoteLlmAttachment {
489    fn validate(&self, index: usize) -> Result<(), RemoteProtocolError> {
490        if self.mime.trim().is_empty() {
491            return Err(RemoteProtocolError::InvalidEnvelope {
492                type_name: "RemoteLlmAttachment",
493                message: format!("attachment at index {index} requires a non-empty mime"),
494            });
495        }
496        if let Some(reference) = &self.reference {
497            reference.validate()?;
498        }
499        Ok(())
500    }
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
504pub struct RemoteAttachmentRef {
505    pub id: String,
506    pub mime: String,
507    pub byte_len: u64,
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub width: Option<u32>,
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub height: Option<u32>,
512    #[serde(default, skip_serializing_if = "Option::is_none")]
513    pub label: Option<String>,
514    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
515    pub metadata: HashMap<String, String>,
516}
517
518impl RemoteAttachmentRef {
519    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
520        require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
521        require_non_empty("RemoteAttachmentRef", "mime", &self.mime)
522    }
523}
524
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
526pub struct RemoteLlmToolSpec {
527    pub name: String,
528    #[serde(default)]
529    pub description: String,
530    #[serde(default = "default_remote_input_schema")]
531    pub input_schema: RemoteSchemaContract,
532    #[serde(default)]
533    pub output_schema: RemoteSchemaContract,
534}
535
536impl RemoteLlmToolSpec {
537    pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
538        require_non_empty("RemoteLlmToolSpec", "name", &self.name)
539    }
540}
541
542#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
543#[serde(rename_all = "snake_case")]
544pub enum RemoteLlmToolChoice {
545    #[default]
546    Auto,
547    None,
548    Required,
549}
550
551#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
552#[serde(tag = "type", rename_all = "snake_case")]
553pub enum RemoteLlmOutputSpec {
554    JsonObject,
555    JsonSchema {
556        name: String,
557        schema: RemoteSchemaContract,
558        strict: bool,
559    },
560}
561
562impl RemoteLlmOutputSpec {
563    fn validate(&self) -> Result<(), RemoteProtocolError> {
564        match self {
565            Self::JsonObject => Ok(()),
566            Self::JsonSchema { name, .. } => {
567                require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
568            }
569        }
570    }
571}
572
573#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
574#[serde(tag = "type", rename_all = "snake_case")]
575pub enum RemoteLlmOutputPart {
576    Text {
577        text: String,
578        #[serde(default, skip_serializing_if = "Option::is_none")]
579        response_meta: Option<RemoteResponseTextMeta>,
580    },
581    Reasoning {
582        text: String,
583        #[serde(default, skip_serializing_if = "Option::is_none")]
584        replay: Option<RemoteProviderReasoningReplay>,
585    },
586    ToolCall {
587        call_id: String,
588        tool_name: String,
589        input_json: String,
590        #[serde(default, skip_serializing_if = "Option::is_none")]
591        replay: Option<RemoteProviderReplayMeta>,
592    },
593}
594
595#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
596#[serde(rename_all = "snake_case")]
597pub enum RemoteLlmTerminalReason {
598    Stop,
599    ToolUse,
600    OutputLimit,
601    ContextOverflow,
602    ContentFilter,
603    ProviderError,
604    Cancelled,
605    #[default]
606    Unknown,
607}
608
609/// Wire mirror of the core `ProviderFailureKind` classification.
610#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
611#[serde(rename_all = "snake_case")]
612pub enum RemoteProviderFailureKind {
613    Transport,
614    Timeout,
615    Http,
616    Stream,
617    Auth,
618    Validation,
619    Quota,
620    Unsupported,
621    #[default]
622    #[serde(other)]
623    Unknown,
624}
625
626#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
627pub struct RemoteProviderMetadata {
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub usage: Option<serde_json::Value>,
630    #[serde(default, skip_serializing_if = "Option::is_none")]
631    pub request_body: Option<String>,
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub http_summary: Option<String>,
634    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
635    pub data: HashMap<String, serde_json::Value>,
636}
637
638impl RemoteProviderMetadata {
639    pub fn is_empty(&self) -> bool {
640        self.usage.is_none()
641            && self.request_body.is_none()
642            && self.http_summary.is_none()
643            && self.data.is_empty()
644    }
645}
646
647#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
648pub struct RemoteDiagnostic {
649    pub kind: String,
650    #[serde(default, skip_serializing_if = "Option::is_none")]
651    pub code: Option<String>,
652    pub message: String,
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub data: Option<serde_json::Value>,
655}