Skip to main content

imagegen_bridge_core/
response.rs

1//! Normalized provider results and progress events.
2
3use std::collections::BTreeMap;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize, de};
7
8use crate::{BridgeError, GenerationParameters, OutputFormat};
9
10/// Outcome of one ordered provider route attempt.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "snake_case")]
13pub enum ProviderAttemptOutcome {
14    /// The route produced the returned response.
15    Succeeded,
16    /// The route failed and routing continued.
17    Failed,
18}
19
20/// Redaction-safe execution trace for one provider route.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct ProviderAttempt {
24    /// Registered provider name.
25    pub provider: String,
26    /// Requested or discovered model when known.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub model: Option<String>,
29    /// Whether this attempt succeeded or failed.
30    pub outcome: ProviderAttemptOutcome,
31    /// Stable error code for a failed attempt.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub error_code: Option<crate::ErrorCode>,
34    /// Time spent on capability discovery, admission, and execution.
35    pub duration_ms: u64,
36}
37
38/// One explicit normalization or fallback applied to a request.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(deny_unknown_fields)]
41pub struct Normalization {
42    /// JSON-style field path.
43    pub field: String,
44    /// Safe serialized requested value.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub requested: Option<serde_json::Value>,
47    /// Safe serialized effective value.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub effective: Option<serde_json::Value>,
50    /// Stable normalization reason.
51    pub reason: String,
52}
53
54/// Generated output payload.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
56#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
57pub enum ImagePayload {
58    /// Base64 JSON response data.
59    B64Json {
60        /// Base64-encoded bytes.
61        b64_json: String,
62    },
63    /// Provider-hosted or bridge-hosted URL.
64    Url {
65        /// Output URL.
66        url: String,
67    },
68    /// Bridge-owned artifact identifier.
69    Artifact {
70        /// Opaque artifact ID.
71        id: String,
72        /// Optional client-safe relative name.
73        #[serde(skip_serializing_if = "Option::is_none")]
74        name: Option<String>,
75    },
76    /// No body was requested.
77    Metadata,
78}
79
80/// Metadata and payload for one generated image.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
82#[schemars(deny_unknown_fields)]
83pub struct GeneratedImage {
84    /// Zero-based requested output index.
85    pub index: u8,
86    /// Output payload or artifact reference.
87    #[serde(flatten)]
88    pub payload: ImagePayload,
89    /// Verified output format.
90    pub format: OutputFormat,
91    /// Verified width in pixels.
92    pub width: u32,
93    /// Verified height in pixels.
94    pub height: u32,
95    /// Verified encoded byte length.
96    pub bytes: u64,
97    /// Lowercase hexadecimal SHA-256 digest.
98    pub sha256: String,
99    /// Provider time for this output when measured independently.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub generation_ms: Option<u64>,
102    /// Portable relative name of an opt-in metadata sidecar.
103    #[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/// Failure for one output in a best-effort multi-image request.
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147#[serde(deny_unknown_fields)]
148pub struct ImageFailure {
149    /// Zero-based requested output index.
150    pub index: u8,
151    /// Safe structured provider error.
152    pub error: BridgeError,
153    /// Provider time spent before the failure.
154    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/// Optional provider usage accounting.
172#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173#[serde(default, deny_unknown_fields)]
174pub struct Usage {
175    /// Input tokens reported by the provider.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub input_tokens: Option<u64>,
178    /// Output tokens reported by the provider.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub output_tokens: Option<u64>,
181    /// Total tokens reported by the provider.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub total_tokens: Option<u64>,
184    /// Provider-specific safe numeric counters.
185    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
186    pub provider: BTreeMap<String, u64>,
187}
188
189/// Session information returned after a request.
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
191#[serde(deny_unknown_fields)]
192pub struct SessionMetadata {
193    /// Caller-visible session key when persistent mode was used.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub key: Option<String>,
196    /// Upstream thread ID when policy allows returning it.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub thread_id: Option<String>,
199    /// Whether an existing thread was reused.
200    pub reused: bool,
201}
202
203/// Stage timings in milliseconds.
204#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
205#[serde(default, deny_unknown_fields)]
206pub struct Timings {
207    /// Time waiting for admission.
208    pub queue_ms: u64,
209    /// Time spent loading and validating inputs.
210    pub input_ms: u64,
211    /// Time spent in the provider.
212    pub provider_ms: u64,
213    /// Time spent validating/publishing output.
214    pub artifact_ms: u64,
215    /// Total request time.
216    pub total_ms: u64,
217}
218
219/// Complete normalized response.
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
221#[serde(deny_unknown_fields)]
222pub struct ImageResponse {
223    /// Stable bridge request ID.
224    pub id: String,
225    /// Unix timestamp at completion.
226    pub created: u64,
227    /// Provider that executed the request.
228    pub provider: String,
229    /// Effective provider model.
230    pub model: String,
231    /// Requested generation parameters.
232    pub requested: GenerationParameters,
233    /// Effective generation parameters.
234    pub effective: GenerationParameters,
235    /// Explicit normalizations and fallbacks.
236    #[serde(default, skip_serializing_if = "Vec::is_empty")]
237    pub normalizations: Vec<Normalization>,
238    /// Ordered provider attempts, included when fallback routing was requested or used.
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub attempts: Vec<ProviderAttempt>,
241    /// Generated images.
242    pub data: Vec<GeneratedImage>,
243    /// Per-output failures returned only by best-effort multi-image requests.
244    #[serde(default, skip_serializing_if = "Vec::is_empty")]
245    pub failures: Vec<ImageFailure>,
246    /// Revised prompt when available and permitted by policy.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub revised_prompt: Option<String>,
249    /// Provider usage accounting.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub usage: Option<Usage>,
252    /// Session/thread metadata.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub session: Option<SessionMetadata>,
255    /// Request stage timings.
256    pub timings: Timings,
257    /// Stable warning codes.
258    #[serde(default, skip_serializing_if = "Vec::is_empty")]
259    pub warnings: Vec<String>,
260}
261
262/// Incremental provider progress.
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
264#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
265pub enum ProviderEvent {
266    /// Provider accepted the operation.
267    Started,
268    /// Optional textual progress that contains no prompt or credential data.
269    Progress {
270        /// Provider-safe stage label.
271        stage: String,
272    },
273    /// A bounded partial image payload.
274    PartialImage {
275        /// Zero-based output index.
276        index: u8,
277        /// Zero-based partial image index.
278        partial_index: u8,
279        /// Base64-encoded partial bytes.
280        b64_json: String,
281    },
282    /// Provider operation completed.
283    Completed {
284        /// Normalized provider response before artifact publication.
285        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}