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