Compare commits

...
Author SHA1 Message Date
Brandon Thomas 47c4e2d8f0 fix test failures 2026-05-15 17:11:26 -04:00
Brandon Thomas 63a4779094 tests 2026-05-15 17:03:19 -04:00
Brandon Thomas 2c9c29066a more tests 2026-05-15 16:56:49 -04:00
Brandon Thomas 839cb56b9d fix pricing on gpt-image 2026-05-15 16:03:06 -04:00
5 changed files with 838 additions and 410 deletions
@@ -183,17 +183,6 @@ mod tests {
assert!(result.is_ok());
}
#[test]
fn rejects_multiple_media_tokens() {
let tokens = vec![make_token(), make_token()];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
@@ -185,17 +185,6 @@ mod tests {
assert!(result.is_ok());
}
#[test]
fn rejects_multiple_media_tokens() {
let tokens = vec![make_token(), make_token()];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_qwen_edit_2511_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
@@ -65,25 +65,27 @@ mod tests {
fn text_to_image_price_cases() -> Vec<TextPriceCase> {
let mut cases = vec![
(None, None, None, [22, 44, 66, 88]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [2, 3, 4, 5]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [2, 3, 4, 5]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [2, 3, 5, 6]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 7]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 7]),
// Default: High quality, Square
(None, None, None, [16, 32, 48, 64]),
// Presets (no resolution)
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [1, 2, 3, 4]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [1, 2, 3, 4]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [1, 2, 3, 4]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 8]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 8]),
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), None, [2, 4, 6, 8]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 9, 13, 18]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 9, 13, 18]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [7, 13, 19, 25]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 11, 16, 22]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 11, 16, 22]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [7, 14, 21, 28]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [16, 31, 46, 61]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [16, 31, 46, 61]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [22, 44, 66, 88]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [16, 32, 48, 64]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [16, 32, 48, 64]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [24, 47, 71, 94]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 10, 15, 20]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 10, 15, 20]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [5, 10, 15, 20]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 12, 18, 24]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 12, 18, 24]),
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [8, 16, 24, 32]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [15, 30, 45, 60]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [15, 30, 45, 60]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [16, 32, 48, 64]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [20, 40, 60, 80]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [20, 40, 60, 80]),
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [27, 54, 81, 108]),
];
for (size, resolution, low, medium, high) in text_to_image_resolution_price_cases() {
@@ -97,31 +99,34 @@ mod tests {
fn edit_image_price_cases() -> Vec<EditPriceCase> {
let mut cases = vec![
(None, None, None, [22, 44, 66, 88]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [3, 5, 8, 10]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [12, 23, 34, 46]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [42, 83, 124, 166]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [3, 5, 8, 10]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [12, 23, 34, 46]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [42, 83, 124, 166]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [2, 3, 4, 5]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [2, 3, 4, 5]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [2, 3, 5, 6]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [2, 4, 6, 7]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [2, 4, 6, 7]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [2, 4, 6, 8]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [5, 9, 13, 18]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [5, 9, 13, 18]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [7, 13, 19, 25]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [6, 11, 16, 22]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [6, 11, 16, 22]),
// Default: High quality, Square
(None, None, None, [15, 30, 45, 60]),
// Auto (with and without resolution — same cost, conservative max)
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [2, 4, 6, 8]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [10, 20, 30, 40]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [41, 82, 123, 164]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [2, 4, 6, 8]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [10, 20, 30, 40]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [41, 82, 123, 164]),
// Presets (no resolution)
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [1, 2, 3, 4]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [1, 2, 3, 4]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [1, 2, 3, 4]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [1, 2, 3, 4]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [1, 2, 3, 4]),
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [1, 2, 3, 4]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [4, 8, 12, 16]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [4, 8, 12, 16]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [4, 8, 12, 16]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [5, 10, 15, 20]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [5, 10, 15, 20]),
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), None, [7, 14, 21, 28]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [16, 31, 46, 61]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [16, 31, 46, 61]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [22, 44, 66, 88]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [16, 32, 48, 64]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [16, 32, 48, 64]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [24, 47, 71, 94]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [14, 28, 42, 56]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [14, 28, 42, 56]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [15, 30, 45, 60]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [19, 38, 57, 76]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [19, 38, 57, 76]),
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [26, 52, 78, 104]),
];
for (size, resolution, low, medium, high) in edit_image_resolution_price_cases() {
@@ -135,48 +140,60 @@ mod tests {
fn text_to_image_resolution_price_cases() -> Vec<(GptImage2TextToImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
vec![
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 7], [6, 11, 16, 22], [16, 32, 48, 64]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 7], [6, 11, 16, 22], [16, 32, 48, 64]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [2, 3, 5, 6], [7, 13, 19, 25], [22, 44, 66, 88]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [2, 3, 5, 6], [7, 13, 19, 25], [22, 44, 66, 88]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
]
}
fn edit_image_resolution_price_cases() -> Vec<(GptImage2EditImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
text_to_image_resolution_price_cases()
.into_iter()
.map(|(size, resolution, low, medium, high)| {
let edit_size = match size {
GptImage2TextToImageSize::SquareHd => GptImage2EditImageSize::SquareHd,
GptImage2TextToImageSize::Square => GptImage2EditImageSize::Square,
GptImage2TextToImageSize::Portrait4x3 => GptImage2EditImageSize::Portrait4x3,
GptImage2TextToImageSize::Portrait16x9 => GptImage2EditImageSize::Portrait16x9,
GptImage2TextToImageSize::Landscape4x3 => GptImage2EditImageSize::Landscape4x3,
GptImage2TextToImageSize::Landscape16x9 => GptImage2EditImageSize::Landscape16x9,
};
(edit_size, resolution, low, medium, high)
})
.collect()
vec![
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
]
}
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage2TextToImageNumImages, u64); 4] {
@@ -1,67 +1,60 @@
use crate::requests::api::image::common::gpt_image_2_resolution::{compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution};
use crate::requests::api::image::edit::gpt_image_2_edit_image::api::{GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest, GptImage2EditImageSize};
use crate::requests::api::image::common::gpt_image_2_resolution::{
compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution,
};
use crate::requests::api::image::edit::gpt_image_2_edit_image::api::{
GptImage2EditImageNumImages, GptImage2EditImageQuality,
GptImage2EditImageRequest, GptImage2EditImageSize,
};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
const PRICING_TABLE: &[GptImage2PriceRow] = &[GptImage2PriceRow { width: 1024, height: 768, low: 11, medium: 43, high: 151 }, GptImage2PriceRow { width: 1024, height: 1024, low: 15, medium: 61, high: 219 }, GptImage2PriceRow { width: 1024, height: 1536, low: 18, medium: 54, high: 178 }, GptImage2PriceRow { width: 1920, height: 1080, low: 17, medium: 53, high: 158 }, GptImage2PriceRow { width: 2560, height: 1440, low: 19, medium: 68, high: 234 }, GptImage2PriceRow { width: 3840, height: 2160, low: 24, medium: 113, high: 413 }];
#[derive(Copy, Clone)]
struct GptImage2PriceRow {
width: u32,
height: u32,
low: u64,
medium: u64,
high: u64,
}
impl GptImage2PriceRow {
fn price_in_tenths_of_a_cent(self, quality: GptImage2EditImageQuality) -> u64 {
match quality {
GptImage2EditImageQuality::Low => self.low,
GptImage2EditImageQuality::Medium => self.medium,
GptImage2EditImageQuality::High => self.high,
}
}
}
impl FalRequestCostCalculator for GptImage2EditImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
let quality = self.quality.unwrap_or(GptImage2EditImageQuality::High);
let price = match dimensions_for_request(self) {
Some((width, height)) => closest_price_row(width, height).price_in_tenths_of_a_cent(quality),
None => max_price_for_quality(quality),
let total_pixels = estimate_total_pixels(self.image_size, self.resolution);
let megapixels = total_pixels as f64 / 1_000_000.0;
// Fal's GPT Image 2 edit-image pricing has two components:
//
// 1. A base request cost (prompt processing, API overhead)
// 2. A per-pixel generation cost that scales with output resolution
//
// Both components increase with quality level. All rates are in
// tenths of a US cent, derived from Fal's published pricing.
//
// Edit-image is slightly cheaper than text-to-image at the base level,
// but scales at a similar per-pixel rate at medium and high quality.
let (base_tenths, per_megapixel_tenths): (f64, f64) = match quality {
GptImage2EditImageQuality::Low => ( 3.0, 1.0),
GptImage2EditImageQuality::Medium => (25.0, 9.0),
GptImage2EditImageQuality::High => (110.0, 35.0),
};
let total_tenths_of_a_cent = price * num_images(self.num_images);
total_tenths_of_a_cent.div_ceil(10) as UsdCents
let tenths_per_image = (base_tenths + per_megapixel_tenths * megapixels).ceil() as u64;
let cents_per_image = tenths_per_image.div_ceil(10);
let num = num_images_u64(self.num_images);
cents_per_image * num
}
}
fn dimensions_for_request(request: &GptImage2EditImageRequest) -> Option<(u32, u32)> {
match (request.image_size, request.resolution) {
(Some(GptImage2EditImageSize::Auto), _) => None,
(Some(size), Some(resolution)) => {
fn estimate_total_pixels(
image_size: Option<GptImage2EditImageSize>,
resolution: Option<GptImage2Resolution>,
) -> u64 {
match (image_size, resolution) {
// Auto: no meaningful aspect ratio, use conservative max-size estimate
(Some(GptImage2EditImageSize::Auto), _) => 3840 * 2160, // 8,294,400
(Some(size), Some(res)) => {
let aspect = size_to_aspect(size);
let custom = compute_custom_image_size(aspect, resolution);
Some((custom.width, custom.height))
},
(None, Some(resolution)) => {
let custom = compute_custom_image_size(GptImage2AspectRatio::Square, resolution);
Some((custom.width, custom.height))
},
(Some(size), None) => Some(preset_dimensions(size)),
(None, None) => Some(preset_dimensions(GptImage2EditImageSize::Square)),
}
}
fn preset_dimensions(size: GptImage2EditImageSize) -> (u32, u32) {
match size {
GptImage2EditImageSize::SquareHd => (2048, 2048),
GptImage2EditImageSize::Square => (1024, 1024),
GptImage2EditImageSize::Portrait4x3 => (768, 1024),
GptImage2EditImageSize::Portrait16x9 => (1080, 1920),
GptImage2EditImageSize::Landscape4x3 => (1024, 768),
GptImage2EditImageSize::Landscape16x9 => (1920, 1080),
GptImage2EditImageSize::Auto => (3840, 2160),
let dims = compute_custom_image_size(aspect, res);
dims.width as u64 * dims.height as u64
}
(None, Some(res)) => {
let dims = compute_custom_image_size(GptImage2AspectRatio::Square, res);
dims.width as u64 * dims.height as u64
}
(Some(size), None) => preset_pixel_count(size),
(None, None) => preset_pixel_count(GptImage2EditImageSize::Square),
}
}
@@ -77,24 +70,21 @@ fn size_to_aspect(size: GptImage2EditImageSize) -> GptImage2AspectRatio {
}
}
fn closest_price_row(width: u32, height: u32) -> GptImage2PriceRow {
let pixels = width as u64 * height as u64;
PRICING_TABLE
.iter()
.copied()
.min_by_key(|row| {
let row_pixels = row.width as u64 * row.height as u64;
row_pixels.abs_diff(pixels)
})
.expect("GPT Image 2 pricing table cannot be empty")
/// Standard preset pixel counts when no custom resolution is specified.
fn preset_pixel_count(size: GptImage2EditImageSize) -> u64 {
match size {
GptImage2EditImageSize::Square => 1024 * 1024, // 1,048,576
GptImage2EditImageSize::SquareHd => 2048 * 2048, // 4,194,304
GptImage2EditImageSize::Landscape4x3 => 1024 * 768, // 786,432
GptImage2EditImageSize::Landscape16x9 => 1920 * 1080, // 2,073,600
GptImage2EditImageSize::Portrait4x3 => 768 * 1024, // 786,432
GptImage2EditImageSize::Portrait16x9 => 1080 * 1920, // 2,073,600
GptImage2EditImageSize::Auto => 3840 * 2160, // 8,294,400 (conservative)
}
}
fn max_price_for_quality(quality: GptImage2EditImageQuality) -> u64 {
PRICING_TABLE.iter().copied().map(|row| row.price_in_tenths_of_a_cent(quality)).max().expect("GPT Image 2 pricing table cannot be empty")
}
fn num_images(num_images: GptImage2EditImageNumImages) -> u64 {
match num_images {
fn num_images_u64(num: GptImage2EditImageNumImages) -> u64 {
match num {
GptImage2EditImageNumImages::One => 1,
GptImage2EditImageNumImages::Two => 2,
GptImage2EditImageNumImages::Three => 3,
@@ -106,113 +96,348 @@ fn num_images(num_images: GptImage2EditImageNumImages) -> u64 {
mod tests {
use super::*;
const QUALITIES: &[(GptImage2EditImageQuality, u64, u64, u64, u64, u64, u64)] = &[(GptImage2EditImageQuality::Low, 11, 15, 18, 17, 19, 24), (GptImage2EditImageQuality::Medium, 43, 61, 54, 53, 68, 113), (GptImage2EditImageQuality::High, 151, 219, 178, 158, 234, 413)];
const PRESET_CASES: &[(GptImage2EditImageSize, u64)] = &[(GptImage2EditImageSize::Landscape4x3, 0), (GptImage2EditImageSize::Portrait4x3, 0), (GptImage2EditImageSize::Square, 1), (GptImage2EditImageSize::Landscape16x9, 3), (GptImage2EditImageSize::Portrait16x9, 3), (GptImage2EditImageSize::SquareHd, 4)];
const RESOLUTION_CASES: &[(GptImage2EditImageSize, GptImage2Resolution, u64)] = &[
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, 0),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, 4),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, 5),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, 5),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, 0),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, 3),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, 4),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, 5),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, 0),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, 4),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, 5),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, 5),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, 0),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, 3),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, 4),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, 5),
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, 1),
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, 4),
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, 5),
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, 5),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, 1),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, 4),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, 5),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, 5),
];
const SPOT_PRICE_CASES: &[(GptImage2EditImageSize, GptImage2Resolution, GptImage2EditImageNumImages, u64)] = &[
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, GptImage2EditImageNumImages::One, 16),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::Two, 47),
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, GptImage2EditImageNumImages::Three, 124),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::Four, 64),
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, GptImage2EditImageNumImages::Two, 83),
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::One, 24),
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, GptImage2EditImageNumImages::Three, 71),
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, GptImage2EditImageNumImages::Four, 88),
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, GptImage2EditImageNumImages::Four, 166),
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::Three, 71),
];
fn make_request(num_images: GptImage2EditImageNumImages, quality: Option<GptImage2EditImageQuality>, image_size: Option<GptImage2EditImageSize>, resolution: Option<GptImage2Resolution>) -> GptImage2EditImageRequest {
GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, quality, resolution, output_format: None }
fn make_request(
num_images: GptImage2EditImageNumImages,
quality: Option<GptImage2EditImageQuality>,
image_size: Option<GptImage2EditImageSize>,
resolution: Option<GptImage2Resolution>,
) -> GptImage2EditImageRequest {
GptImage2EditImageRequest {
prompt: "test".to_string(),
image_urls: vec!["https://example.com/image.png".to_string()],
num_images,
mask_url: None,
image_size,
resolution,
quality,
output_format: None,
}
}
#[test]
fn cost_defaults_to_high_square() {
assert_eq!(make_request(GptImage2EditImageNumImages::One, None, None, None).calculate_cost_in_cents(), 22);
use GptImage2EditImageNumImages::*;
use GptImage2EditImageQuality::*;
use GptImage2EditImageSize::*;
use GptImage2Resolution::*;
mod preset_pricing_tests {
use super::*;
// Table: (size, low, medium, high) — expected cents per image
const PRESET_CASES: &[(GptImage2EditImageSize, u64, u64, u64)] = &[
// Low Med High
(Landscape4x3, 1, 4, 14),
(Portrait4x3, 1, 4, 14),
(Square, 1, 4, 15),
(Landscape16x9, 1, 5, 19),
(Portrait16x9, 1, 5, 19),
(SquareHd, 1, 7, 26),
(Auto, 2, 10, 41),
];
#[test]
fn preset_costs_at_each_quality() {
for &(size, expected_low, expected_med, expected_high) in PRESET_CASES {
let cases = [
(Low, expected_low),
(Medium, expected_med),
(High, expected_high),
];
for (quality, expected) in cases {
let actual = make_request(One, Some(quality), Some(size), None)
.calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {quality:?}");
}
}
}
#[test]
fn defaults_to_high_quality_square() {
assert_eq!(
make_request(One, None, None, None).calculate_cost_in_cents(),
15,
);
}
}
#[test]
fn cost_rounds_final_request_total_up_to_whole_cents() {
assert_eq!(make_request(GptImage2EditImageNumImages::Four, Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None,).calculate_cost_in_cents(), 5,);
mod custom_resolution_pricing_tests {
use super::*;
// Table: (size, resolution, low, medium, high) — expected cents per image
const RESOLUTION_CASES: &[(GptImage2EditImageSize, GptImage2Resolution, u64, u64, u64)] = &[
// Square aspect
(Square, OneK, 1, 4, 15),
(Square, TwoK, 1, 7, 26),
(Square, ThreeK, 2, 10, 41),
(Square, FourK, 2, 10, 41), // capped at max pixels
// SquareHd (same aspect as Square)
(SquareHd, OneK, 1, 4, 15),
(SquareHd, TwoK, 1, 7, 26),
(SquareHd, ThreeK, 2, 10, 41),
(SquareHd, FourK, 2, 10, 41),
// Landscape 4:3
(Landscape4x3, OneK, 1, 4, 14),
(Landscape4x3, TwoK, 1, 6, 23),
(Landscape4x3, ThreeK, 2, 9, 36),
(Landscape4x3, FourK, 2, 10, 40),
// Landscape 16:9
(Landscape16x9, OneK, 1, 4, 14),
(Landscape16x9, TwoK, 1, 5, 20),
(Landscape16x9, ThreeK, 1, 8, 30),
(Landscape16x9, FourK, 2, 10, 41),
// Portrait 4:3
(Portrait4x3, OneK, 1, 4, 14),
(Portrait4x3, TwoK, 1, 6, 23),
(Portrait4x3, ThreeK, 2, 9, 36),
(Portrait4x3, FourK, 2, 10, 40),
// Portrait 16:9
(Portrait16x9, OneK, 1, 4, 14),
(Portrait16x9, TwoK, 1, 5, 20),
(Portrait16x9, ThreeK, 1, 8, 30),
(Portrait16x9, FourK, 2, 10, 41),
];
#[test]
fn resolution_costs_at_each_quality() {
for &(size, res, expected_low, expected_med, expected_high) in RESOLUTION_CASES {
let cases = [
(Low, expected_low),
(Medium, expected_med),
(High, expected_high),
];
for (quality, expected) in cases {
let actual = make_request(One, Some(quality), Some(size), Some(res))
.calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?}");
}
}
}
#[test]
fn resolution_without_size_defaults_to_square() {
assert_eq!(
make_request(One, Some(High), None, Some(TwoK)).calculate_cost_in_cents(),
make_request(One, Some(High), Some(Square), Some(TwoK)).calculate_cost_in_cents(),
);
}
#[test]
fn auto_ignores_resolution_and_uses_max_estimate() {
// Auto always uses the conservative 3840x2160 estimate
let auto_cost = make_request(One, Some(High), Some(Auto), Some(OneK))
.calculate_cost_in_cents();
let auto_no_res = make_request(One, Some(High), Some(Auto), None)
.calculate_cost_in_cents();
assert_eq!(auto_cost, auto_no_res);
}
}
#[test]
fn preset_pricing_matches_published_table() {
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
mod num_images_tests {
use super::*;
for &(size, row_index) in PRESET_CASES {
let expected = expected_by_row[row_index as usize].div_ceil(10);
assert_eq!(make_request(GptImage2EditImageNumImages::One, Some(quality), Some(size), None).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?}",);
const ALL_BATCH_SIZES: &[(GptImage2EditImageNumImages, u64)] = &[
(One, 1), (Two, 2), (Three, 3), (Four, 4),
];
const ALL_SIZES: &[GptImage2EditImageSize] = &[
Square, SquareHd, Landscape4x3, Landscape16x9, Portrait4x3, Portrait16x9, Auto,
];
const ALL_RESOLUTIONS: &[GptImage2Resolution] = &[OneK, TwoK, ThreeK, FourK];
#[test]
fn batch_scales_linearly_for_all_presets() {
for &size in ALL_SIZES {
let per_image = make_request(One, Some(High), Some(size), None)
.calculate_cost_in_cents();
for &(num, n) in ALL_BATCH_SIZES {
let actual = make_request(num, Some(High), Some(size), None)
.calculate_cost_in_cents();
assert_eq!(actual, per_image * n, "{size:?} x{n}");
}
}
}
#[test]
fn batch_scales_linearly_for_all_resolutions() {
for &size in &[Square, SquareHd, Landscape4x3, Landscape16x9, Portrait4x3, Portrait16x9] {
for &res in ALL_RESOLUTIONS {
let per_image = make_request(One, Some(High), Some(size), Some(res))
.calculate_cost_in_cents();
for &(num, n) in ALL_BATCH_SIZES {
let actual = make_request(num, Some(High), Some(size), Some(res))
.calculate_cost_in_cents();
assert_eq!(actual, per_image * n, "{size:?} {res:?} x{n}");
}
}
}
}
}
#[test]
fn exact_published_rows_match_all_quality_prices() {
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
let cases = [(1024, 768, landscape_4x3), (1024, 1024, square), (1024, 1536, portrait), (1920, 1080, landscape_16x9), (2560, 1440, square_hd), (3840, 2160, four_k)];
mod exhaustive_preset_batch_tests {
use super::*;
for (width, height, expected) in cases {
assert_eq!(closest_price_row(width, height).price_in_tenths_of_a_cent(quality), expected, "quality={quality:?} dimensions={width}x{height}",);
// (size, quality, num_images, expected_cents)
const CASES: &[(GptImage2EditImageSize, GptImage2EditImageQuality, GptImage2EditImageNumImages, u64)] = &[
(Square, Low, One, 1), (Square, Low, Two, 2), (Square, Low, Three, 3), (Square, Low, Four, 4),
(Square, Medium, One, 4), (Square, Medium, Two, 8), (Square, Medium, Three, 12), (Square, Medium, Four, 16),
(Square, High, One, 15), (Square, High, Two, 30), (Square, High, Three, 45), (Square, High, Four, 60),
(SquareHd, Low, One, 1), (SquareHd, Low, Two, 2), (SquareHd, Low, Three, 3), (SquareHd, Low, Four, 4),
(SquareHd, Medium, One, 7), (SquareHd, Medium, Two, 14), (SquareHd, Medium, Three, 21), (SquareHd, Medium, Four, 28),
(SquareHd, High, One, 26), (SquareHd, High, Two, 52), (SquareHd, High, Three, 78), (SquareHd, High, Four, 104),
(Landscape4x3, Low, One, 1), (Landscape4x3, Low, Two, 2), (Landscape4x3, Low, Three, 3), (Landscape4x3, Low, Four, 4),
(Landscape4x3, Medium, One, 4), (Landscape4x3, Medium, Two, 8), (Landscape4x3, Medium, Three, 12), (Landscape4x3, Medium, Four, 16),
(Landscape4x3, High, One, 14), (Landscape4x3, High, Two, 28), (Landscape4x3, High, Three, 42), (Landscape4x3, High, Four, 56),
(Landscape16x9, Low, One, 1), (Landscape16x9, Low, Two, 2), (Landscape16x9, Low, Three, 3), (Landscape16x9, Low, Four, 4),
(Landscape16x9, Medium, One, 5), (Landscape16x9, Medium, Two, 10), (Landscape16x9, Medium, Three, 15), (Landscape16x9, Medium, Four, 20),
(Landscape16x9, High, One, 19), (Landscape16x9, High, Two, 38), (Landscape16x9, High, Three, 57), (Landscape16x9, High, Four, 76),
(Portrait4x3, Low, One, 1), (Portrait4x3, Low, Two, 2), (Portrait4x3, Low, Three, 3), (Portrait4x3, Low, Four, 4),
(Portrait4x3, Medium, One, 4), (Portrait4x3, Medium, Two, 8), (Portrait4x3, Medium, Three, 12), (Portrait4x3, Medium, Four, 16),
(Portrait4x3, High, One, 14), (Portrait4x3, High, Two, 28), (Portrait4x3, High, Three, 42), (Portrait4x3, High, Four, 56),
(Portrait16x9, Low, One, 1), (Portrait16x9, Low, Two, 2), (Portrait16x9, Low, Three, 3), (Portrait16x9, Low, Four, 4),
(Portrait16x9, Medium, One, 5), (Portrait16x9, Medium, Two, 10), (Portrait16x9, Medium, Three, 15), (Portrait16x9, Medium, Four, 20),
(Portrait16x9, High, One, 19), (Portrait16x9, High, Two, 38), (Portrait16x9, High, Three, 57), (Portrait16x9, High, Four, 76),
(Auto, Low, One, 2), (Auto, Low, Two, 4), (Auto, Low, Three, 6), (Auto, Low, Four, 8),
(Auto, Medium, One, 10), (Auto, Medium, Two, 20), (Auto, Medium, Three, 30), (Auto, Medium, Four, 40),
(Auto, High, One, 41), (Auto, High, Two, 82), (Auto, High, Three, 123), (Auto, High, Four, 164),
];
#[test]
fn all_preset_batch_costs() {
for &(size, quality, num, expected) in CASES {
let actual = make_request(num, Some(quality), Some(size), None).calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {quality:?} {num:?}");
}
}
}
#[test]
fn resolution_pricing_covers_aspect_size_resolution_combinations() {
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
mod exhaustive_resolution_batch_tests {
use super::*;
for &(size, resolution, row_index) in RESOLUTION_CASES {
let expected = expected_by_row[row_index as usize].div_ceil(10);
assert_eq!(make_request(GptImage2EditImageNumImages::One, Some(quality), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?} resolution={resolution:?}",);
// (size, resolution, quality, num_images, expected_cents)
const CASES: &[(GptImage2EditImageSize, GptImage2Resolution, GptImage2EditImageQuality, GptImage2EditImageNumImages, u64)] = &[
// Square
(Square, OneK, Low, One, 1), (Square, OneK, Low, Two, 2), (Square, OneK, Low, Three, 3), (Square, OneK, Low, Four, 4),
(Square, OneK, Medium, One, 4), (Square, OneK, Medium, Two, 8), (Square, OneK, Medium, Three, 12), (Square, OneK, Medium, Four, 16),
(Square, OneK, High, One, 15), (Square, OneK, High, Two, 30), (Square, OneK, High, Three, 45), (Square, OneK, High, Four, 60),
(Square, TwoK, Low, One, 1), (Square, TwoK, Low, Two, 2), (Square, TwoK, Low, Three, 3), (Square, TwoK, Low, Four, 4),
(Square, TwoK, Medium, One, 7), (Square, TwoK, Medium, Two, 14), (Square, TwoK, Medium, Three, 21), (Square, TwoK, Medium, Four, 28),
(Square, TwoK, High, One, 26), (Square, TwoK, High, Two, 52), (Square, TwoK, High, Three, 78), (Square, TwoK, High, Four, 104),
(Square, ThreeK, Low, One, 2), (Square, ThreeK, Low, Two, 4), (Square, ThreeK, Low, Three, 6), (Square, ThreeK, Low, Four, 8),
(Square, ThreeK, Medium, One, 10), (Square, ThreeK, Medium, Two, 20), (Square, ThreeK, Medium, Three, 30), (Square, ThreeK, Medium, Four, 40),
(Square, ThreeK, High, One, 41), (Square, ThreeK, High, Two, 82), (Square, ThreeK, High, Three, 123), (Square, ThreeK, High, Four, 164),
(Square, FourK, Low, One, 2), (Square, FourK, Low, Two, 4), (Square, FourK, Low, Three, 6), (Square, FourK, Low, Four, 8),
(Square, FourK, Medium, One, 10), (Square, FourK, Medium, Two, 20), (Square, FourK, Medium, Three, 30), (Square, FourK, Medium, Four, 40),
(Square, FourK, High, One, 41), (Square, FourK, High, Two, 82), (Square, FourK, High, Three, 123), (Square, FourK, High, Four, 164),
// SquareHd
(SquareHd, OneK, Low, One, 1), (SquareHd, OneK, Low, Two, 2), (SquareHd, OneK, Low, Three, 3), (SquareHd, OneK, Low, Four, 4),
(SquareHd, OneK, Medium, One, 4), (SquareHd, OneK, Medium, Two, 8), (SquareHd, OneK, Medium, Three, 12), (SquareHd, OneK, Medium, Four, 16),
(SquareHd, OneK, High, One, 15), (SquareHd, OneK, High, Two, 30), (SquareHd, OneK, High, Three, 45), (SquareHd, OneK, High, Four, 60),
(SquareHd, TwoK, Low, One, 1), (SquareHd, TwoK, Low, Two, 2), (SquareHd, TwoK, Low, Three, 3), (SquareHd, TwoK, Low, Four, 4),
(SquareHd, TwoK, Medium, One, 7), (SquareHd, TwoK, Medium, Two, 14), (SquareHd, TwoK, Medium, Three, 21), (SquareHd, TwoK, Medium, Four, 28),
(SquareHd, TwoK, High, One, 26), (SquareHd, TwoK, High, Two, 52), (SquareHd, TwoK, High, Three, 78), (SquareHd, TwoK, High, Four, 104),
(SquareHd, ThreeK, Low, One, 2), (SquareHd, ThreeK, Low, Two, 4), (SquareHd, ThreeK, Low, Three, 6), (SquareHd, ThreeK, Low, Four, 8),
(SquareHd, ThreeK, Medium, One, 10), (SquareHd, ThreeK, Medium, Two, 20), (SquareHd, ThreeK, Medium, Three, 30), (SquareHd, ThreeK, Medium, Four, 40),
(SquareHd, ThreeK, High, One, 41), (SquareHd, ThreeK, High, Two, 82), (SquareHd, ThreeK, High, Three, 123), (SquareHd, ThreeK, High, Four, 164),
(SquareHd, FourK, Low, One, 2), (SquareHd, FourK, Low, Two, 4), (SquareHd, FourK, Low, Three, 6), (SquareHd, FourK, Low, Four, 8),
(SquareHd, FourK, Medium, One, 10), (SquareHd, FourK, Medium, Two, 20), (SquareHd, FourK, Medium, Three, 30), (SquareHd, FourK, Medium, Four, 40),
(SquareHd, FourK, High, One, 41), (SquareHd, FourK, High, Two, 82), (SquareHd, FourK, High, Three, 123), (SquareHd, FourK, High, Four, 164),
// Landscape4x3
(Landscape4x3, OneK, Low, One, 1), (Landscape4x3, OneK, Low, Two, 2), (Landscape4x3, OneK, Low, Three, 3), (Landscape4x3, OneK, Low, Four, 4),
(Landscape4x3, OneK, Medium, One, 4), (Landscape4x3, OneK, Medium, Two, 8), (Landscape4x3, OneK, Medium, Three, 12), (Landscape4x3, OneK, Medium, Four, 16),
(Landscape4x3, OneK, High, One, 14), (Landscape4x3, OneK, High, Two, 28), (Landscape4x3, OneK, High, Three, 42), (Landscape4x3, OneK, High, Four, 56),
(Landscape4x3, TwoK, Low, One, 1), (Landscape4x3, TwoK, Low, Two, 2), (Landscape4x3, TwoK, Low, Three, 3), (Landscape4x3, TwoK, Low, Four, 4),
(Landscape4x3, TwoK, Medium, One, 6), (Landscape4x3, TwoK, Medium, Two, 12), (Landscape4x3, TwoK, Medium, Three, 18), (Landscape4x3, TwoK, Medium, Four, 24),
(Landscape4x3, TwoK, High, One, 23), (Landscape4x3, TwoK, High, Two, 46), (Landscape4x3, TwoK, High, Three, 69), (Landscape4x3, TwoK, High, Four, 92),
(Landscape4x3, ThreeK, Low, One, 2), (Landscape4x3, ThreeK, Low, Two, 4), (Landscape4x3, ThreeK, Low, Three, 6), (Landscape4x3, ThreeK, Low, Four, 8),
(Landscape4x3, ThreeK, Medium, One, 9), (Landscape4x3, ThreeK, Medium, Two, 18), (Landscape4x3, ThreeK, Medium, Three, 27), (Landscape4x3, ThreeK, Medium, Four, 36),
(Landscape4x3, ThreeK, High, One, 36), (Landscape4x3, ThreeK, High, Two, 72), (Landscape4x3, ThreeK, High, Three, 108), (Landscape4x3, ThreeK, High, Four, 144),
(Landscape4x3, FourK, Low, One, 2), (Landscape4x3, FourK, Low, Two, 4), (Landscape4x3, FourK, Low, Three, 6), (Landscape4x3, FourK, Low, Four, 8),
(Landscape4x3, FourK, Medium, One, 10), (Landscape4x3, FourK, Medium, Two, 20), (Landscape4x3, FourK, Medium, Three, 30), (Landscape4x3, FourK, Medium, Four, 40),
(Landscape4x3, FourK, High, One, 40), (Landscape4x3, FourK, High, Two, 80), (Landscape4x3, FourK, High, Three, 120), (Landscape4x3, FourK, High, Four, 160),
// Landscape16x9
(Landscape16x9, OneK, Low, One, 1), (Landscape16x9, OneK, Low, Two, 2), (Landscape16x9, OneK, Low, Three, 3), (Landscape16x9, OneK, Low, Four, 4),
(Landscape16x9, OneK, Medium, One, 4), (Landscape16x9, OneK, Medium, Two, 8), (Landscape16x9, OneK, Medium, Three, 12), (Landscape16x9, OneK, Medium, Four, 16),
(Landscape16x9, OneK, High, One, 14), (Landscape16x9, OneK, High, Two, 28), (Landscape16x9, OneK, High, Three, 42), (Landscape16x9, OneK, High, Four, 56),
(Landscape16x9, TwoK, Low, One, 1), (Landscape16x9, TwoK, Low, Two, 2), (Landscape16x9, TwoK, Low, Three, 3), (Landscape16x9, TwoK, Low, Four, 4),
(Landscape16x9, TwoK, Medium, One, 5), (Landscape16x9, TwoK, Medium, Two, 10), (Landscape16x9, TwoK, Medium, Three, 15), (Landscape16x9, TwoK, Medium, Four, 20),
(Landscape16x9, TwoK, High, One, 20), (Landscape16x9, TwoK, High, Two, 40), (Landscape16x9, TwoK, High, Three, 60), (Landscape16x9, TwoK, High, Four, 80),
(Landscape16x9, ThreeK, Low, One, 1), (Landscape16x9, ThreeK, Low, Two, 2), (Landscape16x9, ThreeK, Low, Three, 3), (Landscape16x9, ThreeK, Low, Four, 4),
(Landscape16x9, ThreeK, Medium, One, 8), (Landscape16x9, ThreeK, Medium, Two, 16), (Landscape16x9, ThreeK, Medium, Three, 24), (Landscape16x9, ThreeK, Medium, Four, 32),
(Landscape16x9, ThreeK, High, One, 30), (Landscape16x9, ThreeK, High, Two, 60), (Landscape16x9, ThreeK, High, Three, 90), (Landscape16x9, ThreeK, High, Four, 120),
(Landscape16x9, FourK, Low, One, 2), (Landscape16x9, FourK, Low, Two, 4), (Landscape16x9, FourK, Low, Three, 6), (Landscape16x9, FourK, Low, Four, 8),
(Landscape16x9, FourK, Medium, One, 10), (Landscape16x9, FourK, Medium, Two, 20), (Landscape16x9, FourK, Medium, Three, 30), (Landscape16x9, FourK, Medium, Four, 40),
(Landscape16x9, FourK, High, One, 41), (Landscape16x9, FourK, High, Two, 82), (Landscape16x9, FourK, High, Three, 123), (Landscape16x9, FourK, High, Four, 164),
// Portrait4x3
(Portrait4x3, OneK, Low, One, 1), (Portrait4x3, OneK, Low, Two, 2), (Portrait4x3, OneK, Low, Three, 3), (Portrait4x3, OneK, Low, Four, 4),
(Portrait4x3, OneK, Medium, One, 4), (Portrait4x3, OneK, Medium, Two, 8), (Portrait4x3, OneK, Medium, Three, 12), (Portrait4x3, OneK, Medium, Four, 16),
(Portrait4x3, OneK, High, One, 14), (Portrait4x3, OneK, High, Two, 28), (Portrait4x3, OneK, High, Three, 42), (Portrait4x3, OneK, High, Four, 56),
(Portrait4x3, TwoK, Low, One, 1), (Portrait4x3, TwoK, Low, Two, 2), (Portrait4x3, TwoK, Low, Three, 3), (Portrait4x3, TwoK, Low, Four, 4),
(Portrait4x3, TwoK, Medium, One, 6), (Portrait4x3, TwoK, Medium, Two, 12), (Portrait4x3, TwoK, Medium, Three, 18), (Portrait4x3, TwoK, Medium, Four, 24),
(Portrait4x3, TwoK, High, One, 23), (Portrait4x3, TwoK, High, Two, 46), (Portrait4x3, TwoK, High, Three, 69), (Portrait4x3, TwoK, High, Four, 92),
(Portrait4x3, ThreeK, Low, One, 2), (Portrait4x3, ThreeK, Low, Two, 4), (Portrait4x3, ThreeK, Low, Three, 6), (Portrait4x3, ThreeK, Low, Four, 8),
(Portrait4x3, ThreeK, Medium, One, 9), (Portrait4x3, ThreeK, Medium, Two, 18), (Portrait4x3, ThreeK, Medium, Three, 27), (Portrait4x3, ThreeK, Medium, Four, 36),
(Portrait4x3, ThreeK, High, One, 36), (Portrait4x3, ThreeK, High, Two, 72), (Portrait4x3, ThreeK, High, Three, 108), (Portrait4x3, ThreeK, High, Four, 144),
(Portrait4x3, FourK, Low, One, 2), (Portrait4x3, FourK, Low, Two, 4), (Portrait4x3, FourK, Low, Three, 6), (Portrait4x3, FourK, Low, Four, 8),
(Portrait4x3, FourK, Medium, One, 10), (Portrait4x3, FourK, Medium, Two, 20), (Portrait4x3, FourK, Medium, Three, 30), (Portrait4x3, FourK, Medium, Four, 40),
(Portrait4x3, FourK, High, One, 40), (Portrait4x3, FourK, High, Two, 80), (Portrait4x3, FourK, High, Three, 120), (Portrait4x3, FourK, High, Four, 160),
// Portrait16x9
(Portrait16x9, OneK, Low, One, 1), (Portrait16x9, OneK, Low, Two, 2), (Portrait16x9, OneK, Low, Three, 3), (Portrait16x9, OneK, Low, Four, 4),
(Portrait16x9, OneK, Medium, One, 4), (Portrait16x9, OneK, Medium, Two, 8), (Portrait16x9, OneK, Medium, Three, 12), (Portrait16x9, OneK, Medium, Four, 16),
(Portrait16x9, OneK, High, One, 14), (Portrait16x9, OneK, High, Two, 28), (Portrait16x9, OneK, High, Three, 42), (Portrait16x9, OneK, High, Four, 56),
(Portrait16x9, TwoK, Low, One, 1), (Portrait16x9, TwoK, Low, Two, 2), (Portrait16x9, TwoK, Low, Three, 3), (Portrait16x9, TwoK, Low, Four, 4),
(Portrait16x9, TwoK, Medium, One, 5), (Portrait16x9, TwoK, Medium, Two, 10), (Portrait16x9, TwoK, Medium, Three, 15), (Portrait16x9, TwoK, Medium, Four, 20),
(Portrait16x9, TwoK, High, One, 20), (Portrait16x9, TwoK, High, Two, 40), (Portrait16x9, TwoK, High, Three, 60), (Portrait16x9, TwoK, High, Four, 80),
(Portrait16x9, ThreeK, Low, One, 1), (Portrait16x9, ThreeK, Low, Two, 2), (Portrait16x9, ThreeK, Low, Three, 3), (Portrait16x9, ThreeK, Low, Four, 4),
(Portrait16x9, ThreeK, Medium, One, 8), (Portrait16x9, ThreeK, Medium, Two, 16), (Portrait16x9, ThreeK, Medium, Three, 24), (Portrait16x9, ThreeK, Medium, Four, 32),
(Portrait16x9, ThreeK, High, One, 30), (Portrait16x9, ThreeK, High, Two, 60), (Portrait16x9, ThreeK, High, Three, 90), (Portrait16x9, ThreeK, High, Four, 120),
(Portrait16x9, FourK, Low, One, 2), (Portrait16x9, FourK, Low, Two, 4), (Portrait16x9, FourK, Low, Three, 6), (Portrait16x9, FourK, Low, Four, 8),
(Portrait16x9, FourK, Medium, One, 10), (Portrait16x9, FourK, Medium, Two, 20), (Portrait16x9, FourK, Medium, Three, 30), (Portrait16x9, FourK, Medium, Four, 40),
(Portrait16x9, FourK, High, One, 41), (Portrait16x9, FourK, High, Two, 82), (Portrait16x9, FourK, High, Three, 123), (Portrait16x9, FourK, High, Four, 164),
];
#[test]
fn all_resolution_batch_costs() {
for &(size, res, quality, num, expected) in CASES {
let actual = make_request(num, Some(quality), Some(size), Some(res)).calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?} {num:?}");
}
}
}
#[test]
fn high_quality_spot_prices_match_expected_cents() {
for &(size, resolution, num_images, expected) in SPOT_PRICE_CASES {
assert_eq!(make_request(num_images, Some(GptImage2EditImageQuality::High), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "size={size:?} resolution={resolution:?} num_images={num_images:?}",);
mod monotonicity_tests {
use super::*;
#[test]
fn higher_quality_costs_more() {
for &size in &[Square, Landscape16x9, Portrait4x3, Auto] {
let low = make_request(One, Some(Low), Some(size), None).calculate_cost_in_cents();
let med = make_request(One, Some(Medium), Some(size), None).calculate_cost_in_cents();
let high = make_request(One, Some(High), Some(size), None).calculate_cost_in_cents();
assert!(low <= med, "{size:?}: low ({low}) should be <= medium ({med})");
assert!(med <= high, "{size:?}: medium ({med}) should be <= high ({high})");
}
}
}
#[test]
fn auto_uses_highest_published_price_for_quality() {
assert_eq!(make_request(GptImage2EditImageNumImages::Two, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK),).calculate_cost_in_cents(), 83,);
}
#[test]
fn resolution_without_image_size_defaults_to_square_aspect() {
assert_eq!(make_request(GptImage2EditImageNumImages::Two, Some(GptImage2EditImageQuality::High), None, Some(GptImage2Resolution::FourK),).calculate_cost_in_cents(), 83,);
#[test]
fn higher_resolution_costs_at_least_as_much() {
for &size in &[Square, Landscape16x9, Portrait4x3] {
let resolutions = [OneK, TwoK, ThreeK, FourK];
for pair in resolutions.windows(2) {
let lower = make_request(One, Some(High), Some(size), Some(pair[0]))
.calculate_cost_in_cents();
let higher = make_request(One, Some(High), Some(size), Some(pair[1]))
.calculate_cost_in_cents();
assert!(
lower <= higher,
"{size:?}: {pair:?} — lower res ({lower}¢) should be <= higher res ({higher}¢)",
);
}
}
}
}
}
@@ -1,63 +1,58 @@
use crate::requests::api::image::common::gpt_image_2_resolution::{compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution};
use crate::requests::api::image::text::gpt_image_2_text_to_image::api::{GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageRequest, GptImage2TextToImageSize};
use crate::requests::api::image::common::gpt_image_2_resolution::{
compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution,
};
use crate::requests::api::image::text::gpt_image_2_text_to_image::api::{
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
GptImage2TextToImageRequest, GptImage2TextToImageSize,
};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
const PRICING_TABLE: &[GptImage2PriceRow] = &[GptImage2PriceRow { width: 1024, height: 768, low: 11, medium: 43, high: 151 }, GptImage2PriceRow { width: 1024, height: 1024, low: 15, medium: 61, high: 219 }, GptImage2PriceRow { width: 1024, height: 1536, low: 18, medium: 54, high: 178 }, GptImage2PriceRow { width: 1920, height: 1080, low: 17, medium: 53, high: 158 }, GptImage2PriceRow { width: 2560, height: 1440, low: 19, medium: 68, high: 234 }, GptImage2PriceRow { width: 3840, height: 2160, low: 24, medium: 113, high: 413 }];
#[derive(Copy, Clone)]
struct GptImage2PriceRow {
width: u32,
height: u32,
low: u64,
medium: u64,
high: u64,
}
impl GptImage2PriceRow {
fn price_in_tenths_of_a_cent(self, quality: GptImage2TextToImageQuality) -> u64 {
match quality {
GptImage2TextToImageQuality::Low => self.low,
GptImage2TextToImageQuality::Medium => self.medium,
GptImage2TextToImageQuality::High => self.high,
}
}
}
impl FalRequestCostCalculator for GptImage2TextToImageRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
let quality = self.quality.unwrap_or(GptImage2TextToImageQuality::High);
let (width, height) = dimensions_for_request(self);
let price = closest_price_row(width, height).price_in_tenths_of_a_cent(quality);
let total_tenths_of_a_cent = price * num_images(self.num_images);
let total_pixels = estimate_total_pixels(self.image_size, self.resolution);
let megapixels = total_pixels as f64 / 1_000_000.0;
total_tenths_of_a_cent.div_ceil(10) as UsdCents
// Fal's GPT Image 2 text-to-image pricing has two components:
//
// 1. A base request cost (prompt processing, API overhead)
// 2. A per-pixel generation cost that scales with output resolution
//
// Both components increase with quality level. All rates are in
// tenths of a US cent, derived from Fal's published pricing.
//
// The base cost dominates at small resolutions (~1K); the per-pixel
// cost dominates at large resolutions (~4K).
let (base_tenths, per_megapixel_tenths): (f64, f64) = match quality {
GptImage2TextToImageQuality::Low => ( 8.0, 1.5),
GptImage2TextToImageQuality::Medium => (35.0, 9.5),
GptImage2TextToImageQuality::High => (120.0, 35.0),
};
let tenths_per_image = (base_tenths + per_megapixel_tenths * megapixels).ceil() as u64;
let cents_per_image = tenths_per_image.div_ceil(10);
let num = num_images_u64(self.num_images);
cents_per_image * num
}
}
fn dimensions_for_request(request: &GptImage2TextToImageRequest) -> (u32, u32) {
match (request.image_size, request.resolution) {
(Some(size), Some(resolution)) => {
fn estimate_total_pixels(
image_size: Option<GptImage2TextToImageSize>,
resolution: Option<GptImage2Resolution>,
) -> u64 {
match (image_size, resolution) {
(Some(size), Some(res)) => {
let aspect = size_to_aspect(size);
let custom = compute_custom_image_size(aspect, resolution);
(custom.width, custom.height)
},
(None, Some(resolution)) => {
let custom = compute_custom_image_size(GptImage2AspectRatio::Square, resolution);
(custom.width, custom.height)
},
(Some(size), None) => preset_dimensions(size),
(None, None) => preset_dimensions(GptImage2TextToImageSize::Square),
}
}
fn preset_dimensions(size: GptImage2TextToImageSize) -> (u32, u32) {
match size {
GptImage2TextToImageSize::SquareHd => (2048, 2048),
GptImage2TextToImageSize::Square => (1024, 1024),
GptImage2TextToImageSize::Portrait4x3 => (768, 1024),
GptImage2TextToImageSize::Portrait16x9 => (1080, 1920),
GptImage2TextToImageSize::Landscape4x3 => (1024, 768),
GptImage2TextToImageSize::Landscape16x9 => (1920, 1080),
let dims = compute_custom_image_size(aspect, res);
dims.width as u64 * dims.height as u64
}
(None, Some(res)) => {
let dims = compute_custom_image_size(GptImage2AspectRatio::Square, res);
dims.width as u64 * dims.height as u64
}
(Some(size), None) => preset_pixel_count(size),
(None, None) => preset_pixel_count(GptImage2TextToImageSize::Square),
}
}
@@ -72,20 +67,20 @@ fn size_to_aspect(size: GptImage2TextToImageSize) -> GptImage2AspectRatio {
}
}
fn closest_price_row(width: u32, height: u32) -> GptImage2PriceRow {
let pixels = width as u64 * height as u64;
PRICING_TABLE
.iter()
.copied()
.min_by_key(|row| {
let row_pixels = row.width as u64 * row.height as u64;
row_pixels.abs_diff(pixels)
})
.expect("GPT Image 2 pricing table cannot be empty")
/// Standard preset pixel counts when no custom resolution is specified.
fn preset_pixel_count(size: GptImage2TextToImageSize) -> u64 {
match size {
GptImage2TextToImageSize::Square => 1024 * 1024, // 1,048,576
GptImage2TextToImageSize::SquareHd => 2048 * 2048, // 4,194,304
GptImage2TextToImageSize::Landscape4x3 => 1024 * 768, // 786,432
GptImage2TextToImageSize::Landscape16x9 => 1920 * 1080, // 2,073,600
GptImage2TextToImageSize::Portrait4x3 => 768 * 1024, // 786,432
GptImage2TextToImageSize::Portrait16x9 => 1080 * 1920, // 2,073,600
}
}
fn num_images(num_images: GptImage2TextToImageNumImages) -> u64 {
match num_images {
fn num_images_u64(num: GptImage2TextToImageNumImages) -> u64 {
match num {
GptImage2TextToImageNumImages::One => 1,
GptImage2TextToImageNumImages::Two => 2,
GptImage2TextToImageNumImages::Three => 3,
@@ -97,119 +92,332 @@ fn num_images(num_images: GptImage2TextToImageNumImages) -> u64 {
mod tests {
use super::*;
const QUALITIES: &[(GptImage2TextToImageQuality, u64, u64, u64, u64, u64, u64)] = &[
(GptImage2TextToImageQuality::Low, 11, 15, 18, 17, 19, 24),
(GptImage2TextToImageQuality::Medium, 43, 61, 54, 53, 68, 113),
(GptImage2TextToImageQuality::High, 151, 219, 178, 158, 234, 413)
];
const PRESET_CASES: &[(GptImage2TextToImageSize, u64)] = &[
(GptImage2TextToImageSize::Landscape4x3, 0),
(GptImage2TextToImageSize::Portrait4x3, 0),
(GptImage2TextToImageSize::Square, 1),
(GptImage2TextToImageSize::Landscape16x9, 3),
(GptImage2TextToImageSize::Portrait16x9, 3),
(GptImage2TextToImageSize::SquareHd, 4),
];
const RESOLUTION_CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, u64)] = &[
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, 0),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, 4),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, 5),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, 5),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, 0),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, 3),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, 4),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, 5),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, 0),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, 4),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, 5),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, 5),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, 0),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, 3),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, 4),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, 5),
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, 1),
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, 4),
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, 5),
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, 5),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, 1),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, 4),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, 5),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, 5),
];
const SPOT_PRICE_CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, GptImage2TextToImageNumImages, u64)] = &[
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, GptImage2TextToImageNumImages::One, 16),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::Two, 47),
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, GptImage2TextToImageNumImages::Three, 124),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::Four, 64),
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, GptImage2TextToImageNumImages::Two, 83),
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::One, 24),
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, GptImage2TextToImageNumImages::Three, 71),
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, GptImage2TextToImageNumImages::Four, 88),
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, GptImage2TextToImageNumImages::Four, 166),
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::Three, 71),
];
fn make_request(num_images: GptImage2TextToImageNumImages, quality: Option<GptImage2TextToImageQuality>, image_size: Option<GptImage2TextToImageSize>, resolution: Option<GptImage2Resolution>) -> GptImage2TextToImageRequest {
GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, quality, resolution, output_format: None }
fn make_request(
num_images: GptImage2TextToImageNumImages,
quality: Option<GptImage2TextToImageQuality>,
image_size: Option<GptImage2TextToImageSize>,
resolution: Option<GptImage2Resolution>,
) -> GptImage2TextToImageRequest {
GptImage2TextToImageRequest {
prompt: "test".to_string(),
num_images,
image_size,
resolution,
quality,
output_format: None,
}
}
#[test]
fn cost_defaults_to_high_square() {
assert_eq!(make_request(GptImage2TextToImageNumImages::One, None, None, None).calculate_cost_in_cents(), 22);
use GptImage2TextToImageNumImages::*;
use GptImage2TextToImageQuality::*;
use GptImage2TextToImageSize::*;
use GptImage2Resolution::*;
mod preset_pricing_tests {
use super::*;
// Table: (size, low, medium, high) — expected cents per image
const PRESET_CASES: &[(GptImage2TextToImageSize, u64, u64, u64)] = &[
// Low Med High
(Landscape4x3, 1, 5, 15),
(Portrait4x3, 1, 5, 15),
(Square, 1, 5, 16),
(Landscape16x9, 2, 6, 20),
(Portrait16x9, 2, 6, 20),
(SquareHd, 2, 8, 27),
];
#[test]
fn preset_costs_at_each_quality() {
for &(size, expected_low, expected_med, expected_high) in PRESET_CASES {
let cases = [
(Low, expected_low),
(Medium, expected_med),
(High, expected_high),
];
for (quality, expected) in cases {
let actual = make_request(One, Some(quality), Some(size), None)
.calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {quality:?}");
}
}
}
#[test]
fn defaults_to_high_quality_square() {
assert_eq!(
make_request(One, None, None, None).calculate_cost_in_cents(),
16,
);
}
}
#[test]
fn cost_rounds_final_request_total_up_to_whole_cents() {
assert_eq!(make_request(GptImage2TextToImageNumImages::Four, Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None,).calculate_cost_in_cents(), 5,);
mod custom_resolution_pricing_tests {
use super::*;
// Table: (size, resolution, low, medium, high) — expected cents per image
const RESOLUTION_CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, u64, u64, u64)] = &[
// Square aspect
(Square, OneK, 1, 5, 16),
(Square, TwoK, 2, 8, 27),
(Square, ThreeK, 3, 12, 42),
(Square, FourK, 3, 12, 42), // capped at max pixels
// SquareHd (same aspect as Square)
(SquareHd, OneK, 1, 5, 16),
(SquareHd, TwoK, 2, 8, 27),
(SquareHd, ThreeK, 3, 12, 42),
(SquareHd, FourK, 3, 12, 42),
// Landscape 4:3
(Landscape4x3, OneK, 1, 5, 15),
(Landscape4x3, TwoK, 2, 7, 24),
(Landscape4x3, ThreeK, 2, 11, 37),
(Landscape4x3, FourK, 3, 12, 41),
// Landscape 16:9
(Landscape16x9, OneK, 1, 5, 15),
(Landscape16x9, TwoK, 2, 6, 21),
(Landscape16x9, ThreeK, 2, 9, 31),
(Landscape16x9, FourK, 3, 12, 42),
// Portrait 4:3
(Portrait4x3, OneK, 1, 5, 15),
(Portrait4x3, TwoK, 2, 7, 24),
(Portrait4x3, ThreeK, 2, 11, 37),
(Portrait4x3, FourK, 3, 12, 41),
// Portrait 16:9
(Portrait16x9, OneK, 1, 5, 15),
(Portrait16x9, TwoK, 2, 6, 21),
(Portrait16x9, ThreeK, 2, 9, 31),
(Portrait16x9, FourK, 3, 12, 42),
];
#[test]
fn resolution_costs_at_each_quality() {
for &(size, res, expected_low, expected_med, expected_high) in RESOLUTION_CASES {
let cases = [
(Low, expected_low),
(Medium, expected_med),
(High, expected_high),
];
for (quality, expected) in cases {
let actual = make_request(One, Some(quality), Some(size), Some(res))
.calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?}");
}
}
}
#[test]
fn resolution_without_size_defaults_to_square() {
assert_eq!(
make_request(One, Some(High), None, Some(TwoK)).calculate_cost_in_cents(),
make_request(One, Some(High), Some(Square), Some(TwoK)).calculate_cost_in_cents(),
);
}
}
#[test]
fn preset_pricing_matches_published_table() {
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
mod num_images_tests {
use super::*;
for &(size, row_index) in PRESET_CASES {
let expected = expected_by_row[row_index as usize].div_ceil(10);
assert_eq!(make_request(GptImage2TextToImageNumImages::One, Some(quality), Some(size), None).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?}",);
const ALL_BATCH_SIZES: &[(GptImage2TextToImageNumImages, u64)] = &[
(One, 1), (Two, 2), (Three, 3), (Four, 4),
];
const ALL_SIZES: &[GptImage2TextToImageSize] = &[
Square, SquareHd, Landscape4x3, Landscape16x9, Portrait4x3, Portrait16x9,
];
const ALL_RESOLUTIONS: &[GptImage2Resolution] = &[OneK, TwoK, ThreeK, FourK];
#[test]
fn batch_scales_linearly_for_all_presets() {
for &size in ALL_SIZES {
let per_image = make_request(One, Some(High), Some(size), None)
.calculate_cost_in_cents();
for &(num, n) in ALL_BATCH_SIZES {
let actual = make_request(num, Some(High), Some(size), None)
.calculate_cost_in_cents();
assert_eq!(actual, per_image * n, "{size:?} x{n}");
}
}
}
#[test]
fn batch_scales_linearly_for_all_resolutions() {
for &size in ALL_SIZES {
for &res in ALL_RESOLUTIONS {
let per_image = make_request(One, Some(High), Some(size), Some(res))
.calculate_cost_in_cents();
for &(num, n) in ALL_BATCH_SIZES {
let actual = make_request(num, Some(High), Some(size), Some(res))
.calculate_cost_in_cents();
assert_eq!(actual, per_image * n, "{size:?} {res:?} x{n}");
}
}
}
}
}
#[test]
fn exact_published_rows_match_all_quality_prices() {
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
let cases = [(1024, 768, landscape_4x3), (1024, 1024, square), (1024, 1536, portrait), (1920, 1080, landscape_16x9), (2560, 1440, square_hd), (3840, 2160, four_k)];
mod exhaustive_preset_batch_tests {
use super::*;
for (width, height, expected) in cases {
assert_eq!(closest_price_row(width, height).price_in_tenths_of_a_cent(quality), expected, "quality={quality:?} dimensions={width}x{height}",);
// (size, quality, num_images, expected_cents)
const CASES: &[(GptImage2TextToImageSize, GptImage2TextToImageQuality, GptImage2TextToImageNumImages, u64)] = &[
(Square, Low, One, 1), (Square, Low, Two, 2), (Square, Low, Three, 3), (Square, Low, Four, 4),
(Square, Medium, One, 5), (Square, Medium, Two, 10), (Square, Medium, Three, 15), (Square, Medium, Four, 20),
(Square, High, One, 16), (Square, High, Two, 32), (Square, High, Three, 48), (Square, High, Four, 64),
(SquareHd, Low, One, 2), (SquareHd, Low, Two, 4), (SquareHd, Low, Three, 6), (SquareHd, Low, Four, 8),
(SquareHd, Medium, One, 8), (SquareHd, Medium, Two, 16), (SquareHd, Medium, Three, 24), (SquareHd, Medium, Four, 32),
(SquareHd, High, One, 27), (SquareHd, High, Two, 54), (SquareHd, High, Three, 81), (SquareHd, High, Four, 108),
(Landscape4x3, Low, One, 1), (Landscape4x3, Low, Two, 2), (Landscape4x3, Low, Three, 3), (Landscape4x3, Low, Four, 4),
(Landscape4x3, Medium, One, 5), (Landscape4x3, Medium, Two, 10), (Landscape4x3, Medium, Three, 15), (Landscape4x3, Medium, Four, 20),
(Landscape4x3, High, One, 15), (Landscape4x3, High, Two, 30), (Landscape4x3, High, Three, 45), (Landscape4x3, High, Four, 60),
(Landscape16x9, Low, One, 2), (Landscape16x9, Low, Two, 4), (Landscape16x9, Low, Three, 6), (Landscape16x9, Low, Four, 8),
(Landscape16x9, Medium, One, 6), (Landscape16x9, Medium, Two, 12), (Landscape16x9, Medium, Three, 18), (Landscape16x9, Medium, Four, 24),
(Landscape16x9, High, One, 20), (Landscape16x9, High, Two, 40), (Landscape16x9, High, Three, 60), (Landscape16x9, High, Four, 80),
(Portrait4x3, Low, One, 1), (Portrait4x3, Low, Two, 2), (Portrait4x3, Low, Three, 3), (Portrait4x3, Low, Four, 4),
(Portrait4x3, Medium, One, 5), (Portrait4x3, Medium, Two, 10), (Portrait4x3, Medium, Three, 15), (Portrait4x3, Medium, Four, 20),
(Portrait4x3, High, One, 15), (Portrait4x3, High, Two, 30), (Portrait4x3, High, Three, 45), (Portrait4x3, High, Four, 60),
(Portrait16x9, Low, One, 2), (Portrait16x9, Low, Two, 4), (Portrait16x9, Low, Three, 6), (Portrait16x9, Low, Four, 8),
(Portrait16x9, Medium, One, 6), (Portrait16x9, Medium, Two, 12), (Portrait16x9, Medium, Three, 18), (Portrait16x9, Medium, Four, 24),
(Portrait16x9, High, One, 20), (Portrait16x9, High, Two, 40), (Portrait16x9, High, Three, 60), (Portrait16x9, High, Four, 80),
];
#[test]
fn all_preset_batch_costs() {
for &(size, quality, num, expected) in CASES {
let actual = make_request(num, Some(quality), Some(size), None).calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {quality:?} {num:?}");
}
}
}
#[test]
fn resolution_pricing_covers_aspect_size_resolution_combinations() {
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
mod exhaustive_resolution_batch_tests {
use super::*;
for &(size, resolution, row_index) in RESOLUTION_CASES {
let expected = expected_by_row[row_index as usize].div_ceil(10);
assert_eq!(make_request(GptImage2TextToImageNumImages::One, Some(quality), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?} resolution={resolution:?}",);
// (size, resolution, quality, num_images, expected_cents)
const CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, GptImage2TextToImageQuality, GptImage2TextToImageNumImages, u64)] = &[
// Square
(Square, OneK, Low, One, 1), (Square, OneK, Low, Two, 2), (Square, OneK, Low, Three, 3), (Square, OneK, Low, Four, 4),
(Square, OneK, Medium, One, 5), (Square, OneK, Medium, Two, 10), (Square, OneK, Medium, Three, 15), (Square, OneK, Medium, Four, 20),
(Square, OneK, High, One, 16), (Square, OneK, High, Two, 32), (Square, OneK, High, Three, 48), (Square, OneK, High, Four, 64),
(Square, TwoK, Low, One, 2), (Square, TwoK, Low, Two, 4), (Square, TwoK, Low, Three, 6), (Square, TwoK, Low, Four, 8),
(Square, TwoK, Medium, One, 8), (Square, TwoK, Medium, Two, 16), (Square, TwoK, Medium, Three, 24), (Square, TwoK, Medium, Four, 32),
(Square, TwoK, High, One, 27), (Square, TwoK, High, Two, 54), (Square, TwoK, High, Three, 81), (Square, TwoK, High, Four, 108),
(Square, ThreeK, Low, One, 3), (Square, ThreeK, Low, Two, 6), (Square, ThreeK, Low, Three, 9), (Square, ThreeK, Low, Four, 12),
(Square, ThreeK, Medium, One, 12), (Square, ThreeK, Medium, Two, 24), (Square, ThreeK, Medium, Three, 36), (Square, ThreeK, Medium, Four, 48),
(Square, ThreeK, High, One, 42), (Square, ThreeK, High, Two, 84), (Square, ThreeK, High, Three, 126), (Square, ThreeK, High, Four, 168),
(Square, FourK, Low, One, 3), (Square, FourK, Low, Two, 6), (Square, FourK, Low, Three, 9), (Square, FourK, Low, Four, 12),
(Square, FourK, Medium, One, 12), (Square, FourK, Medium, Two, 24), (Square, FourK, Medium, Three, 36), (Square, FourK, Medium, Four, 48),
(Square, FourK, High, One, 42), (Square, FourK, High, Two, 84), (Square, FourK, High, Three, 126), (Square, FourK, High, Four, 168),
// SquareHd
(SquareHd, OneK, Low, One, 1), (SquareHd, OneK, Low, Two, 2), (SquareHd, OneK, Low, Three, 3), (SquareHd, OneK, Low, Four, 4),
(SquareHd, OneK, Medium, One, 5), (SquareHd, OneK, Medium, Two, 10), (SquareHd, OneK, Medium, Three, 15), (SquareHd, OneK, Medium, Four, 20),
(SquareHd, OneK, High, One, 16), (SquareHd, OneK, High, Two, 32), (SquareHd, OneK, High, Three, 48), (SquareHd, OneK, High, Four, 64),
(SquareHd, TwoK, Low, One, 2), (SquareHd, TwoK, Low, Two, 4), (SquareHd, TwoK, Low, Three, 6), (SquareHd, TwoK, Low, Four, 8),
(SquareHd, TwoK, Medium, One, 8), (SquareHd, TwoK, Medium, Two, 16), (SquareHd, TwoK, Medium, Three, 24), (SquareHd, TwoK, Medium, Four, 32),
(SquareHd, TwoK, High, One, 27), (SquareHd, TwoK, High, Two, 54), (SquareHd, TwoK, High, Three, 81), (SquareHd, TwoK, High, Four, 108),
(SquareHd, ThreeK, Low, One, 3), (SquareHd, ThreeK, Low, Two, 6), (SquareHd, ThreeK, Low, Three, 9), (SquareHd, ThreeK, Low, Four, 12),
(SquareHd, ThreeK, Medium, One, 12), (SquareHd, ThreeK, Medium, Two, 24), (SquareHd, ThreeK, Medium, Three, 36), (SquareHd, ThreeK, Medium, Four, 48),
(SquareHd, ThreeK, High, One, 42), (SquareHd, ThreeK, High, Two, 84), (SquareHd, ThreeK, High, Three, 126), (SquareHd, ThreeK, High, Four, 168),
(SquareHd, FourK, Low, One, 3), (SquareHd, FourK, Low, Two, 6), (SquareHd, FourK, Low, Three, 9), (SquareHd, FourK, Low, Four, 12),
(SquareHd, FourK, Medium, One, 12), (SquareHd, FourK, Medium, Two, 24), (SquareHd, FourK, Medium, Three, 36), (SquareHd, FourK, Medium, Four, 48),
(SquareHd, FourK, High, One, 42), (SquareHd, FourK, High, Two, 84), (SquareHd, FourK, High, Three, 126), (SquareHd, FourK, High, Four, 168),
// Landscape4x3
(Landscape4x3, OneK, Low, One, 1), (Landscape4x3, OneK, Low, Two, 2), (Landscape4x3, OneK, Low, Three, 3), (Landscape4x3, OneK, Low, Four, 4),
(Landscape4x3, OneK, Medium, One, 5), (Landscape4x3, OneK, Medium, Two, 10), (Landscape4x3, OneK, Medium, Three, 15), (Landscape4x3, OneK, Medium, Four, 20),
(Landscape4x3, OneK, High, One, 15), (Landscape4x3, OneK, High, Two, 30), (Landscape4x3, OneK, High, Three, 45), (Landscape4x3, OneK, High, Four, 60),
(Landscape4x3, TwoK, Low, One, 2), (Landscape4x3, TwoK, Low, Two, 4), (Landscape4x3, TwoK, Low, Three, 6), (Landscape4x3, TwoK, Low, Four, 8),
(Landscape4x3, TwoK, Medium, One, 7), (Landscape4x3, TwoK, Medium, Two, 14), (Landscape4x3, TwoK, Medium, Three, 21), (Landscape4x3, TwoK, Medium, Four, 28),
(Landscape4x3, TwoK, High, One, 24), (Landscape4x3, TwoK, High, Two, 48), (Landscape4x3, TwoK, High, Three, 72), (Landscape4x3, TwoK, High, Four, 96),
(Landscape4x3, ThreeK, Low, One, 2), (Landscape4x3, ThreeK, Low, Two, 4), (Landscape4x3, ThreeK, Low, Three, 6), (Landscape4x3, ThreeK, Low, Four, 8),
(Landscape4x3, ThreeK, Medium, One, 11), (Landscape4x3, ThreeK, Medium, Two, 22), (Landscape4x3, ThreeK, Medium, Three, 33), (Landscape4x3, ThreeK, Medium, Four, 44),
(Landscape4x3, ThreeK, High, One, 37), (Landscape4x3, ThreeK, High, Two, 74), (Landscape4x3, ThreeK, High, Three, 111), (Landscape4x3, ThreeK, High, Four, 148),
(Landscape4x3, FourK, Low, One, 3), (Landscape4x3, FourK, Low, Two, 6), (Landscape4x3, FourK, Low, Three, 9), (Landscape4x3, FourK, Low, Four, 12),
(Landscape4x3, FourK, Medium, One, 12), (Landscape4x3, FourK, Medium, Two, 24), (Landscape4x3, FourK, Medium, Three, 36), (Landscape4x3, FourK, Medium, Four, 48),
(Landscape4x3, FourK, High, One, 41), (Landscape4x3, FourK, High, Two, 82), (Landscape4x3, FourK, High, Three, 123), (Landscape4x3, FourK, High, Four, 164),
// Landscape16x9
(Landscape16x9, OneK, Low, One, 1), (Landscape16x9, OneK, Low, Two, 2), (Landscape16x9, OneK, Low, Three, 3), (Landscape16x9, OneK, Low, Four, 4),
(Landscape16x9, OneK, Medium, One, 5), (Landscape16x9, OneK, Medium, Two, 10), (Landscape16x9, OneK, Medium, Three, 15), (Landscape16x9, OneK, Medium, Four, 20),
(Landscape16x9, OneK, High, One, 15), (Landscape16x9, OneK, High, Two, 30), (Landscape16x9, OneK, High, Three, 45), (Landscape16x9, OneK, High, Four, 60),
(Landscape16x9, TwoK, Low, One, 2), (Landscape16x9, TwoK, Low, Two, 4), (Landscape16x9, TwoK, Low, Three, 6), (Landscape16x9, TwoK, Low, Four, 8),
(Landscape16x9, TwoK, Medium, One, 6), (Landscape16x9, TwoK, Medium, Two, 12), (Landscape16x9, TwoK, Medium, Three, 18), (Landscape16x9, TwoK, Medium, Four, 24),
(Landscape16x9, TwoK, High, One, 21), (Landscape16x9, TwoK, High, Two, 42), (Landscape16x9, TwoK, High, Three, 63), (Landscape16x9, TwoK, High, Four, 84),
(Landscape16x9, ThreeK, Low, One, 2), (Landscape16x9, ThreeK, Low, Two, 4), (Landscape16x9, ThreeK, Low, Three, 6), (Landscape16x9, ThreeK, Low, Four, 8),
(Landscape16x9, ThreeK, Medium, One, 9), (Landscape16x9, ThreeK, Medium, Two, 18), (Landscape16x9, ThreeK, Medium, Three, 27), (Landscape16x9, ThreeK, Medium, Four, 36),
(Landscape16x9, ThreeK, High, One, 31), (Landscape16x9, ThreeK, High, Two, 62), (Landscape16x9, ThreeK, High, Three, 93), (Landscape16x9, ThreeK, High, Four, 124),
(Landscape16x9, FourK, Low, One, 3), (Landscape16x9, FourK, Low, Two, 6), (Landscape16x9, FourK, Low, Three, 9), (Landscape16x9, FourK, Low, Four, 12),
(Landscape16x9, FourK, Medium, One, 12), (Landscape16x9, FourK, Medium, Two, 24), (Landscape16x9, FourK, Medium, Three, 36), (Landscape16x9, FourK, Medium, Four, 48),
(Landscape16x9, FourK, High, One, 42), (Landscape16x9, FourK, High, Two, 84), (Landscape16x9, FourK, High, Three, 126), (Landscape16x9, FourK, High, Four, 168),
// Portrait4x3
(Portrait4x3, OneK, Low, One, 1), (Portrait4x3, OneK, Low, Two, 2), (Portrait4x3, OneK, Low, Three, 3), (Portrait4x3, OneK, Low, Four, 4),
(Portrait4x3, OneK, Medium, One, 5), (Portrait4x3, OneK, Medium, Two, 10), (Portrait4x3, OneK, Medium, Three, 15), (Portrait4x3, OneK, Medium, Four, 20),
(Portrait4x3, OneK, High, One, 15), (Portrait4x3, OneK, High, Two, 30), (Portrait4x3, OneK, High, Three, 45), (Portrait4x3, OneK, High, Four, 60),
(Portrait4x3, TwoK, Low, One, 2), (Portrait4x3, TwoK, Low, Two, 4), (Portrait4x3, TwoK, Low, Three, 6), (Portrait4x3, TwoK, Low, Four, 8),
(Portrait4x3, TwoK, Medium, One, 7), (Portrait4x3, TwoK, Medium, Two, 14), (Portrait4x3, TwoK, Medium, Three, 21), (Portrait4x3, TwoK, Medium, Four, 28),
(Portrait4x3, TwoK, High, One, 24), (Portrait4x3, TwoK, High, Two, 48), (Portrait4x3, TwoK, High, Three, 72), (Portrait4x3, TwoK, High, Four, 96),
(Portrait4x3, ThreeK, Low, One, 2), (Portrait4x3, ThreeK, Low, Two, 4), (Portrait4x3, ThreeK, Low, Three, 6), (Portrait4x3, ThreeK, Low, Four, 8),
(Portrait4x3, ThreeK, Medium, One, 11), (Portrait4x3, ThreeK, Medium, Two, 22), (Portrait4x3, ThreeK, Medium, Three, 33), (Portrait4x3, ThreeK, Medium, Four, 44),
(Portrait4x3, ThreeK, High, One, 37), (Portrait4x3, ThreeK, High, Two, 74), (Portrait4x3, ThreeK, High, Three, 111), (Portrait4x3, ThreeK, High, Four, 148),
(Portrait4x3, FourK, Low, One, 3), (Portrait4x3, FourK, Low, Two, 6), (Portrait4x3, FourK, Low, Three, 9), (Portrait4x3, FourK, Low, Four, 12),
(Portrait4x3, FourK, Medium, One, 12), (Portrait4x3, FourK, Medium, Two, 24), (Portrait4x3, FourK, Medium, Three, 36), (Portrait4x3, FourK, Medium, Four, 48),
(Portrait4x3, FourK, High, One, 41), (Portrait4x3, FourK, High, Two, 82), (Portrait4x3, FourK, High, Three, 123), (Portrait4x3, FourK, High, Four, 164),
// Portrait16x9
(Portrait16x9, OneK, Low, One, 1), (Portrait16x9, OneK, Low, Two, 2), (Portrait16x9, OneK, Low, Three, 3), (Portrait16x9, OneK, Low, Four, 4),
(Portrait16x9, OneK, Medium, One, 5), (Portrait16x9, OneK, Medium, Two, 10), (Portrait16x9, OneK, Medium, Three, 15), (Portrait16x9, OneK, Medium, Four, 20),
(Portrait16x9, OneK, High, One, 15), (Portrait16x9, OneK, High, Two, 30), (Portrait16x9, OneK, High, Three, 45), (Portrait16x9, OneK, High, Four, 60),
(Portrait16x9, TwoK, Low, One, 2), (Portrait16x9, TwoK, Low, Two, 4), (Portrait16x9, TwoK, Low, Three, 6), (Portrait16x9, TwoK, Low, Four, 8),
(Portrait16x9, TwoK, Medium, One, 6), (Portrait16x9, TwoK, Medium, Two, 12), (Portrait16x9, TwoK, Medium, Three, 18), (Portrait16x9, TwoK, Medium, Four, 24),
(Portrait16x9, TwoK, High, One, 21), (Portrait16x9, TwoK, High, Two, 42), (Portrait16x9, TwoK, High, Three, 63), (Portrait16x9, TwoK, High, Four, 84),
(Portrait16x9, ThreeK, Low, One, 2), (Portrait16x9, ThreeK, Low, Two, 4), (Portrait16x9, ThreeK, Low, Three, 6), (Portrait16x9, ThreeK, Low, Four, 8),
(Portrait16x9, ThreeK, Medium, One, 9), (Portrait16x9, ThreeK, Medium, Two, 18), (Portrait16x9, ThreeK, Medium, Three, 27), (Portrait16x9, ThreeK, Medium, Four, 36),
(Portrait16x9, ThreeK, High, One, 31), (Portrait16x9, ThreeK, High, Two, 62), (Portrait16x9, ThreeK, High, Three, 93), (Portrait16x9, ThreeK, High, Four, 124),
(Portrait16x9, FourK, Low, One, 3), (Portrait16x9, FourK, Low, Two, 6), (Portrait16x9, FourK, Low, Three, 9), (Portrait16x9, FourK, Low, Four, 12),
(Portrait16x9, FourK, Medium, One, 12), (Portrait16x9, FourK, Medium, Two, 24), (Portrait16x9, FourK, Medium, Three, 36), (Portrait16x9, FourK, Medium, Four, 48),
(Portrait16x9, FourK, High, One, 42), (Portrait16x9, FourK, High, Two, 84), (Portrait16x9, FourK, High, Three, 126), (Portrait16x9, FourK, High, Four, 168),
];
#[test]
fn all_resolution_batch_costs() {
for &(size, res, quality, num, expected) in CASES {
let actual = make_request(num, Some(quality), Some(size), Some(res)).calculate_cost_in_cents();
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?} {num:?}");
}
}
}
#[test]
fn high_quality_spot_prices_match_expected_cents() {
for &(size, resolution, num_images, expected) in SPOT_PRICE_CASES {
assert_eq!(make_request(num_images, Some(GptImage2TextToImageQuality::High), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "size={size:?} resolution={resolution:?} num_images={num_images:?}",);
}
}
mod monotonicity_tests {
use super::*;
#[test]
fn resolution_without_image_size_defaults_to_square_aspect() {
assert_eq!(make_request(GptImage2TextToImageNumImages::Two, Some(GptImage2TextToImageQuality::High), None, Some(GptImage2Resolution::FourK),).calculate_cost_in_cents(), 83,);
#[test]
fn higher_quality_costs_more() {
for &size in &[Square, Landscape16x9, Portrait4x3] {
let low = make_request(One, Some(Low), Some(size), None).calculate_cost_in_cents();
let med = make_request(One, Some(Medium), Some(size), None).calculate_cost_in_cents();
let high = make_request(One, Some(High), Some(size), None).calculate_cost_in_cents();
assert!(low <= med, "{size:?}: low ({low}) should be <= medium ({med})");
assert!(med <= high, "{size:?}: medium ({med}) should be <= high ({high})");
}
}
#[test]
fn higher_resolution_costs_at_least_as_much() {
for &size in &[Square, Landscape16x9, Portrait4x3] {
let resolutions = [OneK, TwoK, ThreeK, FourK];
for pair in resolutions.windows(2) {
let lower = make_request(One, Some(High), Some(size), Some(pair[0]))
.calculate_cost_in_cents();
let higher = make_request(One, Some(High), Some(size), Some(pair[1]))
.calculate_cost_in_cents();
assert!(
lower <= higher,
"{size:?}: {pair:?} — lower res ({lower}¢) should be <= higher res ({higher}¢)",
);
}
}
}
}
}