Skip to main content

systemprompt_models/ai/
response.rs

1//! The canonical inference response shape.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use super::tools::{CallToolResult, ToolCall};
7use serde::{Deserialize, Serialize};
8use systemprompt_identifiers::AiRequestId;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AiResponse {
12    pub request_id: AiRequestId,
13    pub content: String,
14    pub provider: String,
15    pub model: String,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub tokens_used: Option<u32>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub input_tokens: Option<u32>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub output_tokens: Option<u32>,
22    pub latency_ms: u64,
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub tool_calls: Vec<ToolCall>,
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    pub tool_results: Vec<CallToolResult>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub finish_reason: Option<String>,
29    pub cache_hit: bool,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub cache_read_tokens: Option<u32>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub cache_creation_tokens: Option<u32>,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub reasoning_tokens: Option<u32>,
36    pub is_streaming: bool,
37}
38
39impl AiResponse {
40    pub const fn new(
41        request_id: AiRequestId,
42        content: String,
43        provider: String,
44        model: String,
45    ) -> Self {
46        Self {
47            request_id,
48            content,
49            provider,
50            model,
51            tokens_used: None,
52            input_tokens: None,
53            output_tokens: None,
54            latency_ms: 0,
55            tool_calls: Vec::new(),
56            tool_results: Vec::new(),
57            finish_reason: None,
58            cache_hit: false,
59            cache_read_tokens: None,
60            cache_creation_tokens: None,
61            reasoning_tokens: None,
62            is_streaming: false,
63        }
64    }
65
66    pub const fn with_tokens(mut self, tokens_used: u32) -> Self {
67        self.tokens_used = Some(tokens_used);
68        self
69    }
70
71    pub const fn with_latency(mut self, latency_ms: u64) -> Self {
72        self.latency_ms = latency_ms;
73        self
74    }
75
76    pub const fn with_streaming(mut self, is_streaming: bool) -> Self {
77        self.is_streaming = is_streaming;
78        self
79    }
80
81    pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
82        self.tool_calls = tool_calls;
83        self
84    }
85
86    pub fn with_tool_results(mut self, tool_results: Vec<CallToolResult>) -> Self {
87        self.tool_results = tool_results;
88        self
89    }
90
91    pub const fn has_tool_calls(&self) -> bool {
92        !self.tool_calls.is_empty()
93    }
94
95    pub const fn has_tool_results(&self) -> bool {
96        !self.tool_results.is_empty()
97    }
98}
99
100#[derive(Debug, Clone)]
101pub enum StreamChunk {
102    Text(String),
103    Usage {
104        input_tokens: Option<u32>,
105        output_tokens: Option<u32>,
106        tokens_used: Option<u32>,
107        cache_read_tokens: Option<u32>,
108        cache_creation_tokens: Option<u32>,
109        reasoning_tokens: Option<u32>,
110        finish_reason: Option<String>,
111    },
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct WebSource {
116    pub title: String,
117    pub uri: String,
118    pub relevance: f32,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UrlMetadata {
123    pub retrieved_url: String,
124    pub url_retrieval_status: String,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SearchGroundedResponse {
129    pub content: String,
130    pub sources: Vec<WebSource>,
131    pub confidence_scores: Vec<f32>,
132    pub web_search_queries: Vec<String>,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub url_context_metadata: Option<Vec<UrlMetadata>>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub tokens_used: Option<u32>,
137    pub latency_ms: u64,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub finish_reason: Option<String>,
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub safety_ratings: Option<Vec<SafetyRating>>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct SafetyRating {
147    pub category: String,
148    pub probability: String,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub blocked: Option<bool>,
151}