diff --git a/src/Providers/Gemini/Handlers/Structured.php b/src/Providers/Gemini/Handlers/Structured.php index 6d4870f59..9625504c7 100644 --- a/src/Providers/Gemini/Handlers/Structured.php +++ b/src/Providers/Gemini/Handlers/Structured.php @@ -68,7 +68,11 @@ public function handle(Request $request): StructuredResponse return match ($finishReason) { FinishReason::ToolCalls => $this->handleToolCalls($data, $request), FinishReason::Stop, FinishReason::Length => $this->handleStop($data, $request, $finishReason), - default => throw new PrismException('Gemini: unhandled finish reason'), + default => throw new PrismException(sprintf( + 'Gemini: unhandled finish reason "%s" (raw: %s)', + $finishReason->value, + data_get($data, 'candidates.0.finishReason', 'unknown'), + )), }; } diff --git a/src/Providers/Gemini/Handlers/Text.php b/src/Providers/Gemini/Handlers/Text.php index fce051b67..ae5df02f3 100644 --- a/src/Providers/Gemini/Handlers/Text.php +++ b/src/Providers/Gemini/Handlers/Text.php @@ -59,7 +59,11 @@ public function handle(Request $request): TextResponse return match ($finishReason) { FinishReason::ToolCalls => $this->handleToolCalls($data, $request), FinishReason::Stop, FinishReason::Length => $this->handleStop($data, $request, $finishReason), - default => throw new PrismException('Gemini: unhandled finish reason'), + default => throw new PrismException(sprintf( + 'Gemini: unhandled finish reason "%s" (raw: %s)', + $finishReason->value, + data_get($data, 'candidates.0.finishReason', 'unknown'), + )), }; } diff --git a/src/Providers/Groq/Handlers/Text.php b/src/Providers/Groq/Handlers/Text.php index a88c3007e..aaa11e4dc 100644 --- a/src/Providers/Groq/Handlers/Text.php +++ b/src/Providers/Groq/Handlers/Text.php @@ -51,7 +51,11 @@ public function handle(Request $request): TextResponse return match ($finishReason) { FinishReason::ToolCalls => $this->handleToolCalls($data, $request, $response), FinishReason::Stop, FinishReason::Length => $this->handleStop($data, $request, $response, $finishReason), - default => throw new PrismException('Groq: unhandled finish reason'), + default => throw new PrismException(sprintf( + 'Groq: unhandled finish reason "%s" (raw: %s)', + $finishReason->value, + data_get($data, 'choices.0.finish_reason', 'unknown'), + )), }; } diff --git a/tests/Fixtures/gemini/generate-structured-content-filter-1.json b/tests/Fixtures/gemini/generate-structured-content-filter-1.json new file mode 100644 index 000000000..8cb28c4d6 --- /dev/null +++ b/tests/Fixtures/gemini/generate-structured-content-filter-1.json @@ -0,0 +1,27 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "" + } + ], + "role": "model" + }, + "finishReason": "SAFETY", + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "HIGH" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "candidatesTokenCount": 0, + "totalTokenCount": 10 + }, + "modelVersion": "gemini-1.5-flash" +} diff --git a/tests/Fixtures/gemini/generate-text-content-filter-1.json b/tests/Fixtures/gemini/generate-text-content-filter-1.json new file mode 100644 index 000000000..8cb28c4d6 --- /dev/null +++ b/tests/Fixtures/gemini/generate-text-content-filter-1.json @@ -0,0 +1,27 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "" + } + ], + "role": "model" + }, + "finishReason": "SAFETY", + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "HIGH" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "candidatesTokenCount": 0, + "totalTokenCount": 10 + }, + "modelVersion": "gemini-1.5-flash" +} diff --git a/tests/Providers/Gemini/GeminiStructuredTest.php b/tests/Providers/Gemini/GeminiStructuredTest.php index c05af645f..e16bc3843 100644 --- a/tests/Providers/Gemini/GeminiStructuredTest.php +++ b/tests/Providers/Gemini/GeminiStructuredTest.php @@ -7,6 +7,7 @@ use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Http; use Prism\Prism\Enums\Provider; +use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Facades\Prism; use Prism\Prism\Schema\AnyOfSchema; use Prism\Prism\Schema\ArraySchema; @@ -404,3 +405,21 @@ expect($response->steps[0]->additionalContent['thoughtSummaries'])->toBeArray(); expect($response->steps[0]->additionalContent['thoughtSummaries'][0])->toContain('Let me think about'); }); + +it('includes finish reason details in exception for content filter on structured output', function (): void { + FixtureResponse::fakeResponseSequence('*', 'gemini/generate-structured-content-filter'); + + expect(fn () => Prism::structured() + ->using(Provider::Gemini, 'gemini-1.5-flash') + ->withSchema(new ObjectSchema( + 'output', + 'the output object', + [ + new StringSchema('answer', 'The answer', true), + ], + requiredFields: ['answer'], + )) + ->withPrompt('Test prompt') + ->asStructured() + )->toThrow(PrismException::class, 'Gemini: unhandled finish reason "content-filter" (raw: SAFETY)'); +}); diff --git a/tests/Providers/Gemini/GeminiTextTest.php b/tests/Providers/Gemini/GeminiTextTest.php index 354d86cb8..92d63f767 100644 --- a/tests/Providers/Gemini/GeminiTextTest.php +++ b/tests/Providers/Gemini/GeminiTextTest.php @@ -482,6 +482,18 @@ function (Request $request): bool { }); }); +describe('Error handling for Gemini', function (): void { + it('includes finish reason details in exception for content filter', function (): void { + FixtureResponse::fakeResponseSequence('*', 'gemini/generate-text-content-filter'); + + expect(fn () => Prism::text() + ->using(Provider::Gemini, 'gemini-1.5-flash') + ->withPrompt('Test prompt') + ->asText() + )->toThrow(PrismException::class, 'Gemini: unhandled finish reason "content-filter" (raw: SAFETY)'); + }); +}); + describe('Cache support for Gemini', function (): void { it('can use a cache object with a text request', function (): void { FixtureResponse::fakeResponseSequence('*', 'gemini/use-cache-with-text');