1use 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 #[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#[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#[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#[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#[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
493 media_type: Option<String>,
494 },
495}
496
497impl RemoteAttachmentSource {
498 pub(crate) fn validate(&self, _index: usize) -> Result<(), RemoteProtocolError> {
499 match self {
500 Self::Inline {
501 media_type,
502 data_base64,
503 } => {
504 require_non_empty("RemoteAttachmentSource::Inline", "media_type", media_type)?;
505 validate_media_type("RemoteAttachmentSource::Inline", media_type)?;
506 require_non_empty("RemoteAttachmentSource::Inline", "data_base64", data_base64)
507 }
508 Self::Stored { attachment_ref } => attachment_ref.validate(),
509 Self::ExternalUrl { media_type, url } => {
510 require_non_empty(
511 "RemoteAttachmentSource::ExternalUrl",
512 "media_type",
513 media_type,
514 )?;
515 validate_media_type("RemoteAttachmentSource::ExternalUrl", media_type)?;
516 require_non_empty("RemoteAttachmentSource::ExternalUrl", "url", url)
517 }
518 Self::ProviderFile {
519 provider_scope,
520 id,
521 media_type,
522 } => {
523 provider_scope.validate()?;
524 require_non_empty("RemoteAttachmentSource::ProviderFile", "id", id)?;
525 if let Some(media_type) = media_type {
526 require_non_empty(
527 "RemoteAttachmentSource::ProviderFile",
528 "media_type",
529 media_type,
530 )?;
531 validate_media_type("RemoteAttachmentSource::ProviderFile", media_type)?;
532 }
533 Ok(())
534 }
535 }
536 }
537}
538
539#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
540pub struct RemoteProviderFileScope {
541 pub provider: String,
542 pub credential_scope: String,
543}
544
545impl RemoteProviderFileScope {
546 fn validate(&self) -> Result<(), RemoteProtocolError> {
547 require_non_empty("RemoteProviderFileScope", "provider", &self.provider)?;
548 require_non_empty(
549 "RemoteProviderFileScope",
550 "credential_scope",
551 &self.credential_scope,
552 )
553 }
554}
555
556#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
557pub struct RemoteAttachmentRef {
558 pub id: String,
559 pub media_type: String,
560 pub byte_len: u64,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub type_metadata: Option<RemoteAttachmentTypeMetadata>,
563 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub label: Option<String>,
565}
566
567impl RemoteAttachmentRef {
568 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
569 require_non_empty("RemoteAttachmentRef", "id", &self.id)?;
570 require_non_empty("RemoteAttachmentRef", "media_type", &self.media_type)?;
571 validate_media_type("RemoteAttachmentRef", &self.media_type)
572 }
573}
574
575fn validate_media_type(type_name: &'static str, value: &str) -> Result<(), RemoteProtocolError> {
576 let mut pieces = value.split('/');
577 let type_token = pieces.next().unwrap_or_default();
578 let subtype_token = pieces.next().unwrap_or_default();
579 if pieces.next().is_some()
580 || !is_media_type_token(type_token)
581 || !is_media_type_token(subtype_token)
582 {
583 return Err(RemoteProtocolError::InvalidEnvelope {
584 type_name,
585 message: format!("media_type `{value}` must be a syntactically valid type/subtype"),
586 });
587 }
588 Ok(())
589}
590
591fn is_media_type_token(value: &str) -> bool {
592 !value.is_empty()
593 && value.bytes().all(|byte| {
594 byte.is_ascii_alphanumeric()
595 || matches!(
596 byte,
597 b'!' | b'#'
598 | b'$'
599 | b'%'
600 | b'&'
601 | b'\''
602 | b'*'
603 | b'+'
604 | b'-'
605 | b'.'
606 | b'^'
607 | b'_'
608 | b'`'
609 | b'|'
610 | b'~'
611 )
612 })
613}
614
615#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
616#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
617pub enum RemoteAttachmentTypeMetadata {
618 Image {
619 #[serde(default, skip_serializing_if = "Option::is_none")]
620 width: Option<u32>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 height: Option<u32>,
623 },
624}
625
626#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
627pub struct RemoteLlmToolSpec {
628 pub name: String,
629 #[serde(default)]
630 pub description: String,
631 #[serde(default = "default_remote_input_schema")]
632 pub input_schema: RemoteSchemaContract,
633 #[serde(default)]
634 pub output_schema: RemoteSchemaContract,
635}
636
637impl RemoteLlmToolSpec {
638 pub(crate) fn validate(&self) -> Result<(), RemoteProtocolError> {
639 require_non_empty("RemoteLlmToolSpec", "name", &self.name)
640 }
641}
642
643#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
644#[serde(rename_all = "snake_case")]
645pub enum RemoteLlmToolChoice {
646 #[default]
647 Auto,
648 None,
649 Required,
650}
651
652#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
653#[serde(tag = "type", rename_all = "snake_case")]
654pub enum RemoteLlmOutputSpec {
655 JsonObject,
656 JsonSchema {
657 name: String,
658 schema: RemoteSchemaContract,
659 strict: bool,
660 },
661}
662
663impl RemoteLlmOutputSpec {
664 fn validate(&self) -> Result<(), RemoteProtocolError> {
665 match self {
666 Self::JsonObject => Ok(()),
667 Self::JsonSchema { name, .. } => {
668 require_non_empty("RemoteLlmOutputSpec::JsonSchema", "name", name)
669 }
670 }
671 }
672}
673
674#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
675#[serde(tag = "type", rename_all = "snake_case")]
676pub enum RemoteLlmOutputPart {
677 Text {
678 text: String,
679 #[serde(default, skip_serializing_if = "Option::is_none")]
680 response_meta: Option<RemoteResponseTextMeta>,
681 },
682 Reasoning {
683 text: String,
684 #[serde(default, skip_serializing_if = "Option::is_none")]
685 replay: Option<RemoteProviderReasoningReplay>,
686 },
687 ToolCall {
688 call_id: String,
689 tool_name: String,
690 input_json: String,
691 #[serde(default, skip_serializing_if = "Option::is_none")]
692 replay: Option<RemoteProviderReplayMeta>,
693 },
694}
695
696#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
697#[serde(rename_all = "snake_case")]
698pub enum RemoteLlmTerminalReason {
699 Stop,
700 ToolUse,
701 OutputLimit,
702 ContextOverflow,
703 ContentFilter,
704 ProviderError,
705 Cancelled,
706 #[default]
707 Unknown,
708}
709
710#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
712#[serde(rename_all = "snake_case")]
713pub enum RemoteProviderFailureKind {
714 Transport,
715 Timeout,
716 Http,
717 Stream,
718 Auth,
719 Validation,
720 Quota,
721 Unsupported,
722 #[default]
723 #[serde(other)]
724 Unknown,
725}
726
727#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
728pub struct RemoteProviderMetadata {
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub usage: Option<serde_json::Value>,
731 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub request_body: Option<String>,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub http_summary: Option<String>,
735 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
736 pub data: BTreeMap<String, serde_json::Value>,
737}
738
739impl RemoteProviderMetadata {
740 pub fn is_empty(&self) -> bool {
741 self.usage.is_none()
742 && self.request_body.is_none()
743 && self.http_summary.is_none()
744 && self.data.is_empty()
745 }
746}
747
748#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
749pub struct RemoteDiagnostic {
750 pub kind: String,
751 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub code: Option<String>,
753 pub message: String,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub data: Option<serde_json::Value>,
756}