|
171 | 171 | expect($response->json('error.message'))
|
172 | 172 | ->toBe('Prism "nyx" is not registered with PrismServer');
|
173 | 173 | });
|
| 174 | + |
| 175 | +it('handles multimodal messages with image URL', function (): void { |
| 176 | + freezeTime(); |
| 177 | + $generator = Mockery::mock(PendingRequest::class); |
| 178 | + |
| 179 | + $generator->expects('withMessages') |
| 180 | + ->withArgs(function ($messages): bool { |
| 181 | + $message = $messages[0]; |
| 182 | + |
| 183 | + return $message instanceof UserMessage |
| 184 | + && $message->text() === 'What is in this image?' |
| 185 | + && count($message->images()) === 1 |
| 186 | + && $message->images()[0]->url() === 'https://example.com/test.jpg' |
| 187 | + && $message->images()[0]->isUrl(); |
| 188 | + }) |
| 189 | + ->andReturnSelf(); |
| 190 | + |
| 191 | + $textResponse = new Response( |
| 192 | + steps: collect(), |
| 193 | + text: 'I can see a test image.', |
| 194 | + finishReason: FinishReason::Stop, |
| 195 | + toolCalls: [], |
| 196 | + toolResults: [], |
| 197 | + usage: new Usage(15, 12), |
| 198 | + meta: new Meta('cmp_image123', 'gpt-4-vision'), |
| 199 | + responseMessages: collect([ |
| 200 | + new AssistantMessage('I can see a test image.'), |
| 201 | + ]), |
| 202 | + messages: collect(), |
| 203 | + ); |
| 204 | + |
| 205 | + $generator->expects('asText') |
| 206 | + ->andReturn($textResponse); |
| 207 | + |
| 208 | + PrismServer::register( |
| 209 | + 'vision-model', |
| 210 | + fn () => $generator |
| 211 | + ); |
| 212 | + |
| 213 | + /** @var TestResponse */ |
| 214 | + $response = $this->postJson('prism/openai/v1/chat/completions', [ |
| 215 | + 'model' => 'vision-model', |
| 216 | + 'messages' => [[ |
| 217 | + 'role' => 'user', |
| 218 | + 'content' => [ |
| 219 | + ['type' => 'text', 'text' => 'What is in this image?'], |
| 220 | + ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/test.jpg']], |
| 221 | + ], |
| 222 | + ]], |
| 223 | + ]); |
| 224 | + |
| 225 | + $response->assertOk(); |
| 226 | + expect($response->json('choices.0.message.content'))->toBe('I can see a test image.'); |
| 227 | +}); |
| 228 | + |
| 229 | +it('handles multimodal messages with base64 image', function (): void { |
| 230 | + freezeTime(); |
| 231 | + $generator = Mockery::mock(PendingRequest::class); |
| 232 | + |
| 233 | + $base64Image = base64_encode('fake-image-data'); |
| 234 | + |
| 235 | + $generator->expects('withMessages') |
| 236 | + ->withArgs(function ($messages) use ($base64Image): bool { |
| 237 | + $message = $messages[0]; |
| 238 | + |
| 239 | + return $message instanceof UserMessage |
| 240 | + && $message->text() === 'Analyze this screenshot' |
| 241 | + && count($message->images()) === 1 |
| 242 | + && $message->images()[0]->base64() === $base64Image |
| 243 | + && $message->images()[0]->mimeType() === 'image/png' |
| 244 | + && ! $message->images()[0]->isUrl(); |
| 245 | + }) |
| 246 | + ->andReturnSelf(); |
| 247 | + |
| 248 | + $textResponse = new Response( |
| 249 | + steps: collect(), |
| 250 | + text: 'This appears to be a screenshot.', |
| 251 | + finishReason: FinishReason::Stop, |
| 252 | + toolCalls: [], |
| 253 | + toolResults: [], |
| 254 | + usage: new Usage(20, 15), |
| 255 | + meta: new Meta('cmp_base64_123', 'gpt-4-vision'), |
| 256 | + responseMessages: collect([ |
| 257 | + new AssistantMessage('This appears to be a screenshot.'), |
| 258 | + ]), |
| 259 | + messages: collect(), |
| 260 | + ); |
| 261 | + |
| 262 | + $generator->expects('asText') |
| 263 | + ->andReturn($textResponse); |
| 264 | + |
| 265 | + PrismServer::register( |
| 266 | + 'vision-model', |
| 267 | + fn () => $generator |
| 268 | + ); |
| 269 | + |
| 270 | + /** @var TestResponse */ |
| 271 | + $response = $this->postJson('prism/openai/v1/chat/completions', [ |
| 272 | + 'model' => 'vision-model', |
| 273 | + 'messages' => [[ |
| 274 | + 'role' => 'user', |
| 275 | + 'content' => [ |
| 276 | + ['type' => 'text', 'text' => 'Analyze this screenshot'], |
| 277 | + ['type' => 'image_url', 'image_url' => ['url' => "data:image/png;base64,{$base64Image}"]], |
| 278 | + ], |
| 279 | + ]], |
| 280 | + ]); |
| 281 | + |
| 282 | + $response->assertOk(); |
| 283 | + expect($response->json('choices.0.message.content'))->toBe('This appears to be a screenshot.'); |
| 284 | +}); |
| 285 | + |
| 286 | +it('handles multimodal messages with multiple images', function (): void { |
| 287 | + freezeTime(); |
| 288 | + $generator = Mockery::mock(PendingRequest::class); |
| 289 | + |
| 290 | + $generator->expects('withMessages') |
| 291 | + ->withArgs(function ($messages): bool { |
| 292 | + $message = $messages[0]; |
| 293 | + |
| 294 | + return $message instanceof UserMessage |
| 295 | + && $message->text() === 'Compare these two images' |
| 296 | + && count($message->images()) === 2 |
| 297 | + && $message->images()[0]->url() === 'https://example.com/image1.jpg' |
| 298 | + && $message->images()[1]->url() === 'https://example.com/image2.jpg'; |
| 299 | + }) |
| 300 | + ->andReturnSelf(); |
| 301 | + |
| 302 | + $textResponse = new Response( |
| 303 | + steps: collect(), |
| 304 | + text: 'Both images show different scenes.', |
| 305 | + finishReason: FinishReason::Stop, |
| 306 | + toolCalls: [], |
| 307 | + toolResults: [], |
| 308 | + usage: new Usage(25, 18), |
| 309 | + meta: new Meta('cmp_multi123', 'gpt-4-vision'), |
| 310 | + responseMessages: collect([ |
| 311 | + new AssistantMessage('Both images show different scenes.'), |
| 312 | + ]), |
| 313 | + messages: collect(), |
| 314 | + ); |
| 315 | + |
| 316 | + $generator->expects('asText') |
| 317 | + ->andReturn($textResponse); |
| 318 | + |
| 319 | + PrismServer::register( |
| 320 | + 'vision-model', |
| 321 | + fn () => $generator |
| 322 | + ); |
| 323 | + |
| 324 | + /** @var TestResponse */ |
| 325 | + $response = $this->postJson('prism/openai/v1/chat/completions', [ |
| 326 | + 'model' => 'vision-model', |
| 327 | + 'messages' => [[ |
| 328 | + 'role' => 'user', |
| 329 | + 'content' => [ |
| 330 | + ['type' => 'text', 'text' => 'Compare these two images'], |
| 331 | + ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/image1.jpg']], |
| 332 | + ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/image2.jpg']], |
| 333 | + ], |
| 334 | + ]], |
| 335 | + ]); |
| 336 | + |
| 337 | + $response->assertOk(); |
| 338 | + expect($response->json('choices.0.message.content'))->toBe('Both images show different scenes.'); |
| 339 | +}); |
| 340 | + |
| 341 | +it('handles mixed simple and multimodal messages', function (): void { |
| 342 | + freezeTime(); |
| 343 | + $generator = Mockery::mock(PendingRequest::class); |
| 344 | + |
| 345 | + $generator->expects('withMessages') |
| 346 | + ->withArgs(fn ($messages): bool => count($messages) === 2 |
| 347 | + && $messages[0] instanceof UserMessage |
| 348 | + && $messages[0]->text() === 'Hello!' |
| 349 | + && $messages[0]->images() === [] |
| 350 | + && $messages[1] instanceof UserMessage |
| 351 | + && $messages[1]->text() === 'What about this image?' |
| 352 | + && count($messages[1]->images()) === 1) |
| 353 | + ->andReturnSelf(); |
| 354 | + |
| 355 | + $textResponse = new Response( |
| 356 | + steps: collect(), |
| 357 | + text: 'Hello! I can see the image you shared.', |
| 358 | + finishReason: FinishReason::Stop, |
| 359 | + toolCalls: [], |
| 360 | + toolResults: [], |
| 361 | + usage: new Usage(20, 15), |
| 362 | + meta: new Meta('cmp_mixed123', 'gpt-4-vision'), |
| 363 | + responseMessages: collect([ |
| 364 | + new AssistantMessage('Hello! I can see the image you shared.'), |
| 365 | + ]), |
| 366 | + messages: collect(), |
| 367 | + ); |
| 368 | + |
| 369 | + $generator->expects('asText') |
| 370 | + ->andReturn($textResponse); |
| 371 | + |
| 372 | + PrismServer::register( |
| 373 | + 'vision-model', |
| 374 | + fn () => $generator |
| 375 | + ); |
| 376 | + |
| 377 | + /** @var TestResponse */ |
| 378 | + $response = $this->postJson('prism/openai/v1/chat/completions', [ |
| 379 | + 'model' => 'vision-model', |
| 380 | + 'messages' => [ |
| 381 | + [ |
| 382 | + 'role' => 'user', |
| 383 | + 'content' => 'Hello!', |
| 384 | + ], |
| 385 | + [ |
| 386 | + 'role' => 'user', |
| 387 | + 'content' => [ |
| 388 | + ['type' => 'text', 'text' => 'What about this image?'], |
| 389 | + ['type' => 'image_url', 'image_url' => ['url' => 'https://example.com/test.jpg']], |
| 390 | + ], |
| 391 | + ], |
| 392 | + ], |
| 393 | + ]); |
| 394 | + |
| 395 | + $response->assertOk(); |
| 396 | + expect($response->json('choices.0.message.content'))->toBe('Hello! I can see the image you shared.'); |
| 397 | +}); |
0 commit comments