Skip to content

Commit 114f99b

Browse files
committed
fix tests and code
Signed-off-by: Alexandros Pappas <[email protected]>
1 parent 7aa4292 commit 114f99b

File tree

6 files changed

+70
-47
lines changed

6 files changed

+70
-47
lines changed

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiImageApi.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,17 @@ public Flux<OpenAiImageStreamEvent> streamImage(OpenAiImageRequest imageRequest)
134134
.bodyValue(imageRequest)
135135
.retrieve()
136136
.bodyToFlux(String.class)
137-
// Parse the JSON event data
137+
// Parse the JSON event data - each chunk is a complete JSON object
138138
.mapNotNull(content -> {
139139
try {
140-
// SSE format: "event: image_generation.partial_image\ndata: {json}"
141-
// or "event: image_generation.completed\ndata: {json}"
142-
// We only care about the data line
143-
if (content.startsWith("data:")) {
144-
String jsonData = content.substring(5).trim();
145-
return this.objectMapper.readValue(jsonData, OpenAiImageStreamEvent.class);
140+
// Skip empty lines
141+
if (content == null || content.trim().isEmpty()) {
142+
return null;
146143
}
147-
// Skip event type lines and empty lines
148-
return null;
144+
return this.objectMapper.readValue(content.trim(), OpenAiImageStreamEvent.class);
149145
}
150146
catch (JsonProcessingException ex) {
151-
throw new RuntimeException("Failed to parse streaming image event", ex);
147+
throw new RuntimeException("Failed to parse streaming image event: " + content, ex);
152148
}
153149
})
154150
// Complete the stream after receiving the "image_generation.completed" event

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/image/OpenAiImageModelObservationIT.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public class OpenAiImageModelObservationIT {
5858
@Test
5959
void observationForImageOperation() {
6060
var options = OpenAiImageOptions.builder()
61-
.model(OpenAiImageApi.ImageModel.GPT_IMAGE_1_MINI.getValue())
61+
.model(OpenAiImageApi.ImageModel.DALL_E_3.getValue())
6262
.height(1024)
6363
.width(1024)
6464
.responseFormat("url")
@@ -76,12 +76,12 @@ void observationForImageOperation() {
7676
.doesNotHaveAnyRemainingCurrentObservation()
7777
.hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME)
7878
.that()
79-
.hasContextualNameEqualTo("image " + OpenAiImageApi.ImageModel.GPT_IMAGE_1_MINI.getValue())
79+
.hasContextualNameEqualTo("image " + OpenAiImageApi.ImageModel.DALL_E_3.getValue())
8080
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
8181
AiOperationType.IMAGE.value())
8282
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.OPENAI.value())
8383
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
84-
OpenAiImageApi.ImageModel.GPT_IMAGE_1_MINI.getValue())
84+
OpenAiImageApi.ImageModel.DALL_E_3.getValue())
8585
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), "1024x1024")
8686
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_IMAGE_RESPONSE_FORMAT.asString(), "url")
8787
.hasBeenStarted()

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/image/OpenAiImageModelStreamingIT.java

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ void streamImageWithGptImage1MiniAndPartialImages() {
6363
.background("opaque")
6464
.moderation("auto")
6565
.outputCompression(90)
66-
.outputFormat("png")
66+
.outputFormat("jpeg")
6767
.partialImages(2)
6868
.stream(true)
6969
.build();
@@ -77,12 +77,13 @@ void streamImageWithGptImage1MiniAndPartialImages() {
7777
List<ImageResponse> responses = new ArrayList<>();
7878
StepVerifier.create(imageStream)
7979
.recordWith(() -> responses)
80-
.expectNextCount(3) // 2 partial + 1 final
80+
.expectNextCount(1) // At least 1 event (final image is guaranteed)
81+
.thenConsumeWhile(response -> true) // Consume any additional partial images
8182
.verifyComplete();
8283

8384
// Verify we received responses
8485
assertThat(responses).isNotEmpty();
85-
assertThat(responses.size()).isGreaterThanOrEqualTo(1);
86+
assertThat(responses.size()).isGreaterThanOrEqualTo(1); // At least final image
8687

8788
// Verify each response has proper structure
8889
for (ImageResponse response : responses) {
@@ -99,14 +100,15 @@ void streamImageWithGptImage1MiniAndPartialImages() {
99100
@Test
100101
void streamImageWithGptImage1() {
101102
// Create prompt with streaming options and 1 partial image
103+
// Using JPEG for compression < 100 (PNG only supports compression=100)
102104
OpenAiImageOptions options = OpenAiImageOptions.builder()
103105
.model(ImageModel.GPT_IMAGE_1.getValue())
104106
.quality("high")
105107
.size("1024x1024")
106-
.background("transparent")
108+
.background("opaque") // JPEG doesn't support transparency
107109
.moderation("auto")
108110
.outputCompression(85)
109-
.outputFormat("png")
111+
.outputFormat("jpeg")
110112
.partialImages(1)
111113
.stream(true)
112114
.build();
@@ -209,7 +211,7 @@ void streamImageVerifyResponseMetadata() {
209211
.quality("medium")
210212
.size("1024x1024")
211213
.background("transparent")
212-
.outputCompression(90)
214+
.outputCompression(100) // PNG only supports compression=100
213215
.outputFormat("png")
214216
.partialImages(1)
215217
.stream(true)
@@ -219,17 +221,24 @@ void streamImageVerifyResponseMetadata() {
219221

220222
Flux<ImageResponse> imageStream = this.openAiImageModel.stream(prompt);
221223

222-
StepVerifier.create(imageStream.takeLast(1)) // Take only the final event
223-
.assertNext(response -> {
224-
assertThat(response.getResults()).hasSize(1);
225-
assertThat(response.getResult()).isNotNull();
226-
assertThat(response.getResult().getOutput()).isNotNull();
227-
assertThat(response.getResult().getOutput().getB64Json()).isNotEmpty();
228-
229-
// Verify metadata is present
230-
assertThat(response.getMetadata()).isNotNull();
231-
assertThat(response.getMetadata().getCreated()).isNotNull();
232-
})
224+
// Collect all events to ensure we get the final one
225+
StepVerifier.create(imageStream).expectNextMatches(response -> {
226+
// Verify response structure
227+
assertThat(response.getResults()).hasSize(1);
228+
assertThat(response.getResult()).isNotNull();
229+
assertThat(response.getResult().getOutput()).isNotNull();
230+
231+
// Verify image data is present
232+
Image image = response.getResult().getOutput();
233+
assertThat(image.getB64Json()).isNotEmpty();
234+
235+
// Verify metadata is present
236+
assertThat(response.getMetadata()).isNotNull();
237+
assertThat(response.getMetadata().getCreated()).isNotNull();
238+
239+
return true;
240+
})
241+
.thenConsumeWhile(response -> true) // Consume any remaining events
233242
.verifyComplete();
234243
}
235244

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/image/api/OpenAiImageApiIT.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ void createImageWithAllModels(ImageModel model) {
5454
assertThat(response.getBody()).isNotNull();
5555
assertThat(response.getBody().created()).isPositive();
5656
assertThat(response.getBody().data()).isNotEmpty();
57-
assertThat(response.getBody().data().get(0).url()).isNotEmpty();
57+
58+
// GPT-Image models return b64_json, DALL-E models can return url
59+
boolean hasUrl = response.getBody().data().get(0).url() != null
60+
&& !response.getBody().data().get(0).url().isEmpty();
61+
boolean hasB64Json = response.getBody().data().get(0).b64Json() != null
62+
&& !response.getBody().data().get(0).b64Json().isEmpty();
63+
assertThat(hasUrl || hasB64Json).withFailMessage("Response must contain either url or b64_json").isTrue();
5864
}
5965

6066
@ParameterizedTest(name = "{0} : {displayName}")
@@ -166,10 +172,13 @@ void createMultipleImagesWithDallE2() {
166172

167173
@Test
168174
void gptImage1WithAllParameters() {
169-
// Test GPT-Image-1 with all supported parameters
175+
// Test GPT-Image-1 with all supported parameters (except partial which requires
176+
// streaming)
177+
// Using JPEG format to test compression parameter (Compression less than 100 is
178+
// not supported for PNG output format)
170179
OpenAiImageRequest request = new OpenAiImageRequest("A red apple floating in space",
171-
ImageModel.GPT_IMAGE_1.getValue(), 1, "high", null, "1024x1024", null, "test-user", "transparent",
172-
"auto", 85, "png", 3, false);
180+
ImageModel.GPT_IMAGE_1.getValue(), 1, "high", null, "1024x1024", null, "test-user", "opaque", "auto",
181+
85, "jpeg", null, false);
173182

174183
ResponseEntity<OpenAiImageResponse> response = this.openAiImageApi.createImage(request);
175184

@@ -181,10 +190,11 @@ void gptImage1WithAllParameters() {
181190

182191
@Test
183192
void gptImage1MiniWithAllParameters() {
184-
// Test GPT-Image-1-Mini with all supported parameters
193+
// Test GPT-Image-1-Mini with all supported parameters (except partial which
194+
// requires streaming)
185195
OpenAiImageRequest request = new OpenAiImageRequest("A sunset over the ocean",
186196
ImageModel.GPT_IMAGE_1_MINI.getValue(), 1, "medium", null, "1024x1024", null, "test-user", "opaque",
187-
"low", 70, "jpeg", 3, false);
197+
"low", 70, "jpeg", null, false);
188198

189199
ResponseEntity<OpenAiImageResponse> response = this.openAiImageApi.createImage(request);
190200

@@ -195,11 +205,12 @@ void gptImage1MiniWithAllParameters() {
195205
}
196206

197207
@Test
198-
void gptImage1MiniWithAllParametersAndStreamTrue() {
199-
// Test GPT-Image-1-Mini with all supported parameters and stream enabled
208+
void gptImage1MiniWithAllParametersNonStreaming() {
209+
// Test GPT-Image-1-Mini with all supported parameters (non-streaming)
210+
// Note: stream and partial parameters are not used for createImage() method
200211
OpenAiImageRequest request = new OpenAiImageRequest("A colorful abstract pattern",
201212
ImageModel.GPT_IMAGE_1_MINI.getValue(), 1, "auto", null, "1024x1024", null, "test-user", "auto", "auto",
202-
90, "png", 3, true);
213+
null, "jpeg", null, false);
203214

204215
ResponseEntity<OpenAiImageResponse> response = this.openAiImageApi.createImage(request);
205216

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/image/api/OpenAiImageApiStreamingIT.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ public class OpenAiImageApiStreamingIT {
4848
@Test
4949
void streamImageWithGptImage1Mini() {
5050
// Create a streaming request with partial images
51+
// Using JPEG format to support compression < 100
5152
OpenAiImageRequest request = new OpenAiImageRequest("A simple red circle",
5253
ImageModel.GPT_IMAGE_1_MINI.getValue(), 1, "medium", null, "1024x1024", null, "test-user", "opaque",
53-
"auto", 90, "png", 2, true);
54+
"auto", 90, "jpeg", 2, true);
5455

5556
Flux<OpenAiImageStreamEvent> eventStream = this.openAiImageApi.streamImage(request);
5657

@@ -84,10 +85,11 @@ void streamImageWithGptImage1Mini() {
8485
}
8586

8687
@Test
87-
void streamImageWithGptImage1() {
88+
void streamImageWithGptImage1MiniOnePartialImage() {
8889
// Create a streaming request with 1 partial image
89-
OpenAiImageRequest request = new OpenAiImageRequest("A blue square", ImageModel.GPT_IMAGE_1.getValue(), 1,
90-
"high", null, "1024x1024", null, "test-user", "auto", "auto", 100, "png", 1, true);
90+
// Note: Only GPT-Image models support streaming (not DALL-E)
91+
OpenAiImageRequest request = new OpenAiImageRequest("A blue square", ImageModel.GPT_IMAGE_1_MINI.getValue(), 1,
92+
"medium", null, "1024x1024", null, null, "opaque", "auto", 85, "jpeg", 1, true);
9193

9294
Flux<OpenAiImageStreamEvent> eventStream = this.openAiImageApi.streamImage(request);
9395

@@ -124,8 +126,9 @@ void streamImageWithNoPartialImages() {
124126
@Test
125127
void streamImageVerifyMetadata() {
126128
// Test that all metadata fields are populated correctly
129+
// Using compression=100 for PNG (PNG only supports compression=100)
127130
OpenAiImageRequest request = new OpenAiImageRequest("A yellow star", ImageModel.GPT_IMAGE_1_MINI.getValue(), 1,
128-
"medium", null, "1024x1024", null, "test-user", "transparent", "auto", 90, "png", 1, true);
131+
"medium", null, "1024x1024", null, "test-user", "transparent", "auto", 100, "png", 1, true);
129132

130133
Flux<OpenAiImageStreamEvent> eventStream = this.openAiImageApi.streamImage(request);
131134

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/openai-image.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ The prefix `spring.ai.openai.image` is the property prefix that lets you configu
144144
| `spring.ai.openai.image.options.user` | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. | -
145145
| `spring.ai.openai.image.options.background` | The background type for the generated image. Must be one of `transparent`, `opaque`, or `auto` (default). Only supported for GPT-Image models. | -
146146
| `spring.ai.openai.image.options.moderation` | The level of content moderation to apply. Must be one of `low` or `auto` (default). Only supported for GPT-Image models. | -
147-
| `spring.ai.openai.image.options.output_compression` | The compression level for the output image. Integer between 0-100. Only supported for GPT-Image models. | -
147+
| `spring.ai.openai.image.options.output_compression` | The compression level for the output image. Integer between 0-100. Only supported for GPT-Image models. NOTE: Compression less than 100 is not supported for PNG output format. | -
148148
| `spring.ai.openai.image.options.output_format` | The format of the output image. Must be one of `png`, `jpeg`, or `webp`. Only supported for GPT-Image models. | -
149-
| `spring.ai.openai.image.options.partial_images` | The number of partial images to generate during streaming. Must be between 0 and 3. Only supported for GPT-Image models with streaming enabled. | -
149+
| `spring.ai.openai.image.options.partial_images` | The number of partial images to generate during streaming. Must be between 0 and 3. Only supported for GPT-Image models with streaming enabled. NOTE: Partial images are only supported with streaming (`stream=true`). | -
150150
| `spring.ai.openai.image.options.stream` | Enable streaming image generation. When `true`, partial images are sent as they are generated. Only supported for GPT-Image models. | false
151151
|====
152152

@@ -218,13 +218,17 @@ imageStream.subscribe(response -> {
218218
| Parameter | Description | Default
219219

220220
| `stream` | Enable streaming mode. Must be `true` for streaming. | false
221-
| `partialImages` | Number of partial images to send during generation. Must be between 0 and 3. | 0
221+
| `partialImages` | Number of partial images to send during generation. Must be between 0 and 3. Only valid when `stream=true`. | 0
222222
| `outputCompression` | Compression level for streamed images (0-100). Lower values mean faster streaming but larger file sizes. | -
223223
| `outputFormat` | Format for streamed images: `png`, `jpeg`, or `webp`. | -
224224
|====
225225

226226
IMPORTANT: The `partialImages` parameter must be between 0 and 3. Values above 3 will result in an error.
227227

228+
IMPORTANT: Partial images are only supported with streaming enabled (`stream=true`). Setting `partialImages` without `stream=true` will result in an error.
229+
230+
IMPORTANT: Compression levels below 100 are not supported for PNG output format. Use JPEG or WebP format if you need compression below 100.
231+
228232
=== Understanding Partial Images
229233

230234
- **partialImages = 0**: Only the final completed image is streamed

0 commit comments

Comments
 (0)