use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de};
use crate::{BridgeError, GenerationParameters, OutputFormat};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProviderAttemptOutcome {
Succeeded,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ProviderAttempt {
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
pub outcome: ProviderAttemptOutcome,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<crate::ErrorCode>,
pub duration_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Normalization {
pub field: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub requested: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effective: Option<serde_json::Value>,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum ImagePayload {
B64Json {
b64_json: String,
},
Url {
url: String,
},
Artifact {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
Metadata,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct GeneratedImage {
pub index: u8,
#[serde(flatten)]
pub payload: ImagePayload,
pub format: OutputFormat,
pub width: u32,
pub height: u32,
pub bytes: u64,
pub sha256: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub generation_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata_name: Option<String>,
}
impl<'de> Deserialize<'de> for GeneratedImage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut fields = serde_json::Map::<String, serde_json::Value>::deserialize(deserializer)?;
let format = take_required(&mut fields, "format")?;
let index = take_required(&mut fields, "index")?;
let width = take_required(&mut fields, "width")?;
let height = take_required(&mut fields, "height")?;
let bytes = take_required(&mut fields, "bytes")?;
let sha256 = take_required(&mut fields, "sha256")?;
let generation_ms = fields
.remove("generation_ms")
.map(serde_json::from_value)
.transpose()
.map_err(de::Error::custom)?;
let metadata_name = fields
.remove("metadata_name")
.map(serde_json::from_value)
.transpose()
.map_err(de::Error::custom)?;
let payload =
serde_json::from_value(serde_json::Value::Object(fields)).map_err(de::Error::custom)?;
Ok(Self {
index,
payload,
format,
width,
height,
bytes,
sha256,
generation_ms,
metadata_name,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ImageFailure {
pub index: u8,
pub error: BridgeError,
pub generation_ms: u64,
}
fn take_required<T, E>(
fields: &mut serde_json::Map<String, serde_json::Value>,
name: &'static str,
) -> Result<T, E>
where
T: serde::de::DeserializeOwned,
E: de::Error,
{
let value = fields
.remove(name)
.ok_or_else(|| E::custom(format!("missing field `{name}`")))?;
serde_json::from_value(value).map_err(E::custom)
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct Usage {
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub provider: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SessionMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
pub reused: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct Timings {
pub queue_ms: u64,
pub input_ms: u64,
pub provider_ms: u64,
pub artifact_ms: u64,
pub total_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ImageResponse {
pub id: String,
pub created: u64,
pub provider: String,
pub model: String,
pub requested: GenerationParameters,
pub effective: GenerationParameters,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub normalizations: Vec<Normalization>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attempts: Vec<ProviderAttempt>,
pub data: Vec<GeneratedImage>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub failures: Vec<ImageFailure>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revised_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<SessionMetadata>,
pub timings: Timings,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum ProviderEvent {
Started,
Progress {
stage: String,
},
PartialImage {
index: u8,
partial_index: u8,
b64_json: String,
},
Completed {
response: Box<ImageResponse>,
},
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn generated_image_round_trips_and_rejects_unknown_fields() {
let image = GeneratedImage {
index: 0,
payload: ImagePayload::B64Json {
b64_json: "aW1hZ2U=".to_owned(),
},
format: OutputFormat::Png,
width: 1,
height: 1,
bytes: 5,
sha256: "0".repeat(64),
generation_ms: Some(12),
metadata_name: None,
};
let encoded = serde_json::to_value(&image).unwrap();
assert_eq!(
serde_json::from_value::<GeneratedImage>(encoded.clone()).unwrap(),
image
);
let mut unknown = encoded.as_object().unwrap().clone();
unknown.insert("unexpected".to_owned(), serde_json::json!(true));
assert!(
serde_json::from_value::<GeneratedImage>(serde_json::Value::Object(unknown)).is_err()
);
}
}