Skip to content

Commit 08c57bd

Browse files
authored
Merge pull request #426 from BerkayAkpolat/fix/gemini-structured-output
fix: gemini structured output REST API compatibility and error handling
2 parents 05daa65 + 16c5f42 commit 08c57bd

File tree

3 files changed

+57
-10
lines changed

3 files changed

+57
-10
lines changed

src/Providers/Gemini/HandleChat.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use function array_key_exists;
2020
use function trim;
2121
use function is_array;
22+
use function json_encode;
2223

2324
trait HandleChat
2425
{
@@ -60,9 +61,24 @@ public function chat(array|Message $messages): Message
6061
*/
6162
protected function processChatResult(array $result): AssistantMessage
6263
{
63-
$content = $result['candidates'][0]['content'];
64+
if (array_key_exists('error', $result)) {
65+
throw new ProviderException("Gemini API Error: " . ($result['error']['message'] ?? json_encode($result['error'])));
66+
}
67+
68+
if (!array_key_exists('candidates', $result) || empty($result['candidates'])) {
69+
throw new ProviderException("Gemini API returned no candidates. Response: " . json_encode($result));
70+
}
71+
72+
$candidate = $result['candidates'][0];
73+
$finishReason = $candidate['finishReason'] ?? 'UNKNOWN';
74+
75+
if ($finishReason !== 'STOP' && !isset($candidate['content'])) {
76+
throw new ProviderException("Gemini API finished with reason: {$finishReason}. Full response: " . json_encode($result));
77+
}
78+
79+
$content = $candidate['content'];
6480

65-
if (!isset($content['parts']) && isset($result['candidates'][0]['finishReason']) && $result['candidates'][0]['finishReason'] === 'MAX_TOKENS') {
81+
if (!isset($content['parts']) && $finishReason === 'MAX_TOKENS') {
6682
return new AssistantMessage('');
6783
}
6884

src/Providers/Gemini/HandleStream.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public function stream(array|Message $messages): Generator
7171
continue;
7272
}
7373

74+
if (array_key_exists('error', $line)) {
75+
throw new ProviderException("Gemini API Error (Streaming): " . ($line['error']['message'] ?? json_encode($line['error'])));
76+
}
77+
7478
// Save usage information
7579
if (array_key_exists('usageMetadata', $line)) {
7680
$this->streamState->addInputTokens($line['usageMetadata']['promptTokenCount'] ?? 0);

src/Providers/Gemini/HandleStructured.php

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use function end;
1212
use function is_array;
1313
use function json_encode;
14+
use function strtoupper;
1415

1516
trait HandleStructured
1617
{
@@ -33,31 +34,57 @@ public function structured(
3334
$last_message = end($messages);
3435
if ($last_message instanceof Message && $last_message->getRole() === MessageRole::USER->value) {
3536
$last_message->setContents(
36-
$last_message->getContent() . ' Respond using this JSON schema: '.json_encode($response_format)
37+
$last_message->getContent() . ' Respond using this JSON schema: ' . json_encode($response_format)
3738
);
3839
}
3940
} else {
40-
// If there are no tools, we can enforce the structured output.
41-
$this->parameters['generationConfig']['response_schema'] = $this->adaptSchema($response_format);
42-
$this->parameters['generationConfig']['response_mime_type'] = 'application/json';
41+
$this->parameters['generationConfig']['responseSchema'] = $this->adaptSchema($response_format);
42+
$this->parameters['generationConfig']['responseMimeType'] = 'application/json';
4343
}
4444

4545
return $this->chat($messages);
4646
}
4747

4848
/**
49-
* Gemini does not support additionalProperties attribute.
49+
* Adapt Neuron schema to Gemini requirements.
5050
*/
5151
protected function adaptSchema(array $schema): array
5252
{
5353
if (array_key_exists('additionalProperties', $schema)) {
5454
unset($schema['additionalProperties']);
5555
}
5656

57-
foreach ($schema as $key => $value) {
58-
if (is_array($value)) {
59-
$schema[$key] = $this->adaptSchema($value);
57+
if (array_key_exists('type', $schema)) {
58+
if (is_array($schema['type'])) {
59+
foreach ($schema['type'] as $type) {
60+
if ($type !== 'null') {
61+
$schema['type'] = strtoupper((string) $type);
62+
break;
63+
}
64+
}
65+
} else {
66+
$schema['type'] = strtoupper((string) $schema['type']);
6067
}
68+
69+
$schema['type'] = match ($schema['type']) {
70+
'INT' => 'INTEGER',
71+
'BOOL' => 'BOOLEAN',
72+
'DOUBLE', 'FLOAT' => 'NUMBER',
73+
default => $schema['type']
74+
};
75+
}
76+
77+
if (array_key_exists('properties', $schema) && is_array($schema['properties'])) {
78+
foreach ($schema['properties'] as $key => $value) {
79+
if (is_array($value)) {
80+
$schema['properties'][$key] = $this->adaptSchema($value);
81+
}
82+
}
83+
$schema['properties'] = (object) $schema['properties'];
84+
}
85+
86+
if (array_key_exists('items', $schema) && is_array($schema['items'])) {
87+
$schema['items'] = $this->adaptSchema($schema['items']);
6188
}
6289

6390
return $schema;

0 commit comments

Comments
 (0)