1use std::collections::BTreeMap;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize, de};
7
8use crate::{BridgeError, GenerationParameters, OutputFormat};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "snake_case")]
13pub enum ProviderAttemptOutcome {
14 Succeeded,
16 Failed,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct ProviderAttempt {
24 pub provider: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub model: Option<String>,
29 pub outcome: ProviderAttemptOutcome,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub error_code: Option<crate::ErrorCode>,
34 pub duration_ms: u64,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(deny_unknown_fields)]
41pub struct Normalization {
42 pub field: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub requested: Option<serde_json::Value>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub effective: Option<serde_json::Value>,
50 pub reason: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
56#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
57pub enum ImagePayload {
58 B64Json {
60 b64_json: String,
62 },
63 Url {
65 url: String,
67 },
68 Artifact {
70 id: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 name: Option<String>,
75 },
76 Metadata,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
82#[schemars(deny_unknown_fields)]
83pub struct GeneratedImage {
84 pub index: u8,
86 #[serde(flatten)]
88 pub payload: ImagePayload,
89 pub format: OutputFormat,
91 pub width: u32,
93 pub height: u32,
95 pub bytes: u64,
97 pub sha256: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub generation_ms: Option<u64>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub metadata_name: Option<String>,
105}
106
107impl<'de> Deserialize<'de> for GeneratedImage {
108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109 where
110 D: serde::Deserializer<'de>,
111 {
112 let mut fields = serde_json::Map::<String, serde_json::Value>::deserialize(deserializer)?;
113 let format = take_required(&mut fields, "format")?;
114 let index = take_required(&mut fields, "index")?;
115 let width = take_required(&mut fields, "width")?;
116 let height = take_required(&mut fields, "height")?;
117 let bytes = take_required(&mut fields, "bytes")?;
118 let sha256 = take_required(&mut fields, "sha256")?;
119 let generation_ms = fields
120 .remove("generation_ms")
121 .map(serde_json::from_value)
122 .transpose()
123 .map_err(de::Error::custom)?;
124 let metadata_name = fields
125 .remove("metadata_name")
126 .map(serde_json::from_value)
127 .transpose()
128 .map_err(de::Error::custom)?;
129 let payload =
130 serde_json::from_value(serde_json::Value::Object(fields)).map_err(de::Error::custom)?;
131 Ok(Self {
132 index,
133 payload,
134 format,
135 width,
136 height,
137 bytes,
138 sha256,
139 generation_ms,
140 metadata_name,
141 })
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147#[serde(deny_unknown_fields)]
148pub struct ImageFailure {
149 pub index: u8,
151 pub error: BridgeError,
153 pub generation_ms: u64,
155}
156
157fn take_required<T, E>(
158 fields: &mut serde_json::Map<String, serde_json::Value>,
159 name: &'static str,
160) -> Result<T, E>
161where
162 T: serde::de::DeserializeOwned,
163 E: de::Error,
164{
165 let value = fields
166 .remove(name)
167 .ok_or_else(|| E::custom(format!("missing field `{name}`")))?;
168 serde_json::from_value(value).map_err(E::custom)
169}
170
171#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173#[serde(default, deny_unknown_fields)]
174pub struct Usage {
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub input_tokens: Option<u64>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub output_tokens: Option<u64>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub total_tokens: Option<u64>,
184 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
186 pub provider: BTreeMap<String, u64>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
191#[serde(deny_unknown_fields)]
192pub struct SessionMetadata {
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub key: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub thread_id: Option<String>,
199 pub reused: bool,
201}
202
203#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
205#[serde(default, deny_unknown_fields)]
206pub struct Timings {
207 pub queue_ms: u64,
209 pub input_ms: u64,
211 pub provider_ms: u64,
213 pub artifact_ms: u64,
215 pub total_ms: u64,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
221#[serde(deny_unknown_fields)]
222pub struct ImageResponse {
223 pub id: String,
225 pub created: u64,
227 pub provider: String,
229 pub model: String,
231 pub requested: GenerationParameters,
233 pub effective: GenerationParameters,
235 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub normalizations: Vec<Normalization>,
238 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub attempts: Vec<ProviderAttempt>,
241 pub data: Vec<GeneratedImage>,
243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
245 pub failures: Vec<ImageFailure>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub revised_prompt: Option<String>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub usage: Option<Usage>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub session: Option<SessionMetadata>,
255 pub timings: Timings,
257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
259 pub warnings: Vec<String>,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
264#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
265pub enum ProviderEvent {
266 Started,
268 Progress {
270 stage: String,
272 },
273 PartialImage {
275 index: u8,
277 partial_index: u8,
279 b64_json: String,
281 },
282 Completed {
284 response: Box<ImageResponse>,
286 },
287}
288
289#[cfg(test)]
290mod tests {
291 #![allow(clippy::unwrap_used)]
292
293 use super::*;
294
295 #[test]
296 fn generated_image_round_trips_and_rejects_unknown_fields() {
297 let image = GeneratedImage {
298 index: 0,
299 payload: ImagePayload::B64Json {
300 b64_json: "aW1hZ2U=".to_owned(),
301 },
302 format: OutputFormat::Png,
303 width: 1,
304 height: 1,
305 bytes: 5,
306 sha256: "0".repeat(64),
307 generation_ms: Some(12),
308 metadata_name: None,
309 };
310 let encoded = serde_json::to_value(&image).unwrap();
311 assert_eq!(
312 serde_json::from_value::<GeneratedImage>(encoded.clone()).unwrap(),
313 image
314 );
315
316 let mut unknown = encoded.as_object().unwrap().clone();
317 unknown.insert("unexpected".to_owned(), serde_json::json!(true));
318 assert!(
319 serde_json::from_value::<GeneratedImage>(serde_json::Value::Object(unknown)).is_err()
320 );
321 }
322}