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