Skip to content

Commit 582d9f3

Browse files
committed
feat: update Google Gemini models and improve error handling
- Update default model from gemini-1.5-flash to gemini-2.5-flash - Increase default max_tokens from 2000 to 8192 for Gemini 2.5 models - Add support for model selection via environment variables (PDODB_AI_<PROVIDER>_MODEL) - Improve Google API error handling with detailed finishReason checks - Handle MAX_TOKENS, SAFETY, RECITATION finish reasons gracefully - Return partial content with warning when response is truncated - Update documentation with latest available Gemini models - Add helper script check-google-models.php for model discovery
1 parent 98b7922 commit 582d9f3

File tree

4 files changed

+145
-6
lines changed

4 files changed

+145
-6
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,8 @@ Or use CLI:
489489
```bash
490490
# Set API key
491491
export PDODB_AI_OPENAI_KEY=sk-...
492+
export PDODB_AI_OPENAI_MODEL=gpt-4o-mini # Optional: gpt-4, gpt-3.5-turbo
493+
export PDODB_AI_GOOGLE_MODEL=gemini-2.5-flash # Optional: gemini-2.5-pro, gemini-2.0-flash-001, gemini-flash-latest
492494

493495
# Analyze query
494496
pdodb ai analyze "SELECT * FROM users WHERE email = '[email protected]'" \
@@ -498,6 +500,8 @@ pdodb ai analyze "SELECT * FROM users WHERE email = '[email protected]'" \
498500

499501
**Supported Providers:** OpenAI, Anthropic, Google, Microsoft, Ollama (local, no API key)
500502

503+
**Model Selection:** Configure models via environment variables (`PDODB_AI_<PROVIDER>_MODEL`) or config array (`ai.providers.<provider>.model`)
504+
501505
See [AI Analysis Documentation](documentation/05-advanced-features/23-ai-analysis.md) for complete guide.
502506

503507
### Basic CRUD Operations

documentation/05-advanced-features/23-ai-analysis.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ PDOdb supports multiple AI providers, each with their own strengths:
3030
|----------|--------|----------|------------------|
3131
| **OpenAI** | gpt-4o-mini, gpt-4, gpt-3.5-turbo | General analysis, fast responses | Yes |
3232
| **Anthropic** | claude-3-5-sonnet, claude-3-opus | Detailed analysis, long context | Yes |
33-
| **Google** | gemini-pro, gemini-ultra | Multimodal analysis | Yes |
33+
| **Google** | gemini-2.5-flash, gemini-2.5-pro, gemini-2.0-flash-001, gemini-flash-latest, gemini-pro-latest | Multimodal analysis, large context | Yes |
3434
| **Microsoft** | Azure OpenAI models | Enterprise environments | Yes |
3535
| **Ollama** | Any local model (llama2, deepseek-coder, etc.) | Privacy, offline use, no API costs | No |
3636

@@ -52,13 +52,22 @@ export PDODB_AI_ANTHROPIC_KEY=sk-ant-...
5252

5353
# Google
5454
export PDODB_AI_GOOGLE_KEY=...
55+
export PDODB_AI_GOOGLE_MODEL=gemini-2.5-flash # Optional: gemini-2.5-pro, gemini-2.0-flash-001, gemini-flash-latest, gemini-pro-latest
5556

5657
# Microsoft Azure OpenAI
5758
export PDODB_AI_MICROSOFT_KEY=...
59+
export PDODB_AI_MICROSOFT_MODEL=gpt-4 # Optional: model name
5860
# Also configure endpoint via config array (see below)
5961

6062
# Ollama (local, no API key needed)
6163
export PDODB_AI_OLLAMA_URL=http://localhost:11434
64+
export PDODB_AI_OLLAMA_MODEL=llama3.2 # Optional: model name
65+
66+
# OpenAI
67+
export PDODB_AI_OPENAI_MODEL=gpt-4o-mini # Optional: gpt-4, gpt-3.5-turbo
68+
69+
# Anthropic
70+
export PDODB_AI_ANTHROPIC_MODEL=claude-3-5-sonnet-20241022 # Optional: claude-3-opus
6271
```
6372

6473
### Configuration Array
@@ -88,15 +97,41 @@ $db = new PdoDb('mysql', [
8897
'model' => 'claude-3-5-sonnet-20241022',
8998
'temperature' => 0.7,
9099
],
100+
'google' => [
101+
'model' => 'gemini-2.5-flash', # or gemini-2.5-pro, gemini-2.0-flash-001, gemini-flash-latest, gemini-pro-latest
102+
'temperature' => 0.7,
103+
'max_tokens' => 2000,
104+
],
91105
'microsoft' => [
92106
'endpoint' => 'https://your-resource.openai.azure.com',
93107
'deployment' => 'gpt-4',
94108
],
109+
'ollama' => [
110+
'model' => 'llama3.2', # or any local model name
111+
],
95112
],
96113
],
97114
]);
98115
```
99116

117+
### Available Google Gemini Models
118+
119+
Google provides multiple model variants. Recommended models:
120+
121+
- **gemini-2.5-flash** (default) - Stable, fast, versatile model with 1M token context
122+
- **gemini-2.5-pro** - Best for complex tasks, 1M token context, 65K output tokens
123+
- **gemini-2.0-flash-001** - Stable version of Gemini 2.0 Flash (January 2025)
124+
- **gemini-flash-latest** - Always uses the latest Flash model
125+
- **gemini-pro-latest** - Always uses the latest Pro model
126+
127+
For a complete list of available models, use the provided script:
128+
129+
```bash
130+
php check-google-models.php
131+
```
132+
133+
This script will show all models that support `generateContent` with their token limits and descriptions.
134+
100135
### Priority
101136

102137
Configuration priority (highest to lowest):

src/ai/AiConfig.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,24 @@ protected function loadFromEnvironment(): void
110110
} else {
111111
$this->ollamaUrl = 'http://localhost:11434';
112112
}
113+
114+
// Load provider-specific model settings from environment variables
115+
$this->loadProviderModelsFromEnvironment();
116+
}
117+
118+
/**
119+
* Load provider model settings from environment variables.
120+
*/
121+
protected function loadProviderModelsFromEnvironment(): void
122+
{
123+
$providers = ['openai', 'anthropic', 'google', 'microsoft', 'ollama'];
124+
foreach ($providers as $provider) {
125+
$envVar = 'PDODB_AI_' . strtoupper($provider) . '_MODEL';
126+
$model = getenv($envVar);
127+
if ($model !== false && $model !== '') {
128+
$this->setProviderSetting($provider, 'model', $model);
129+
}
130+
}
113131
}
114132

115133
/**

src/ai/providers/GoogleProvider.php

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
class GoogleProvider extends BaseAiProvider
1414
{
1515
private const string API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent'; // v1beta is the latest supported version
16-
private const string DEFAULT_MODEL = 'gemini-pro';
16+
private const string DEFAULT_MODEL = 'gemini-2.5-flash';
1717
private const float DEFAULT_TEMPERATURE = 0.7;
18-
private const int DEFAULT_MAX_TOKENS = 2000;
18+
private const int DEFAULT_MAX_TOKENS = 8192; // Increased default for Gemini 2.5 models (supports up to 65K output tokens)
1919
private const string URL_PARAM_KEY = '?key=';
2020
private const string REQUEST_KEY_CONTENTS = 'contents';
2121
private const string REQUEST_KEY_PARTS = 'parts';
@@ -112,14 +112,96 @@ protected function callApi(string $prompt, string $systemInstruction): string
112112

113113
$response = $this->makeRequest($url, $data);
114114

115-
if (!isset($response[self::RESPONSE_KEY_CANDIDATES][0][self::RESPONSE_KEY_CONTENT][self::REQUEST_KEY_PARTS][0][self::REQUEST_KEY_TEXT])) {
115+
// Check for error in response
116+
if (isset($response['error'])) {
117+
$errorCode = $response['error']['code'] ?? 'unknown';
118+
$errorMessage = $response['error']['message'] ?? 'Unknown error';
116119
throw new QueryException(
117-
'Invalid response format from Google API',
120+
"Google API error ({$errorCode}): {$errorMessage}",
118121
0
119122
);
120123
}
121124

122-
return (string)$response[self::RESPONSE_KEY_CANDIDATES][0][self::RESPONSE_KEY_CONTENT][self::REQUEST_KEY_PARTS][0][self::REQUEST_KEY_TEXT];
125+
// Validate response structure
126+
if (!isset($response[self::RESPONSE_KEY_CANDIDATES])) {
127+
$responseJson = json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
128+
$responseStr = is_string($responseJson) ? substr($responseJson, 0, 500) : 'Unable to encode response';
129+
throw new QueryException(
130+
'Invalid response format from Google API: missing "candidates" key. Response: ' . $responseStr,
131+
0
132+
);
133+
}
134+
135+
if (empty($response[self::RESPONSE_KEY_CANDIDATES])) {
136+
$responseJson = json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
137+
$responseStr = is_string($responseJson) ? substr($responseJson, 0, 500) : 'Unable to encode response';
138+
throw new QueryException(
139+
'Invalid response format from Google API: empty "candidates" array. Response: ' . $responseStr,
140+
0
141+
);
142+
}
143+
144+
$candidate = $response[self::RESPONSE_KEY_CANDIDATES][0];
145+
146+
// Check finish reason
147+
$finishReason = $candidate['finishReason'] ?? null;
148+
if ($finishReason !== null && $finishReason !== 'STOP') {
149+
$reasonMessages = [
150+
'MAX_TOKENS' => 'Response was truncated due to token limit. Consider increasing max_tokens.',
151+
'SAFETY' => 'Response was blocked due to safety filters.',
152+
'RECITATION' => 'Response was blocked due to recitation detection.',
153+
'OTHER' => 'Response was stopped for an unknown reason.',
154+
];
155+
$message = $reasonMessages[$finishReason] ?? "Response was stopped (reason: {$finishReason}).";
156+
157+
// Try to get partial content if available
158+
$content = $candidate[self::RESPONSE_KEY_CONTENT] ?? null;
159+
if ($content !== null && isset($content[self::REQUEST_KEY_PARTS]) && !empty($content[self::REQUEST_KEY_PARTS])) {
160+
$part = $content[self::REQUEST_KEY_PARTS][0];
161+
if (isset($part[self::REQUEST_KEY_TEXT])) {
162+
// Return partial content with warning
163+
return (string)$part[self::REQUEST_KEY_TEXT] . "\n\n[Note: Response was truncated. {$message}]";
164+
}
165+
}
166+
167+
throw new QueryException(
168+
"Google API response issue: {$message}",
169+
0
170+
);
171+
}
172+
173+
if (!isset($candidate[self::RESPONSE_KEY_CONTENT])) {
174+
$responseJson = json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
175+
$responseStr = is_string($responseJson) ? substr($responseJson, 0, 500) : 'Unable to encode response';
176+
throw new QueryException(
177+
'Invalid response format from Google API: missing "content" key in candidate. Response: ' . $responseStr,
178+
0
179+
);
180+
}
181+
182+
$content = $candidate[self::RESPONSE_KEY_CONTENT];
183+
184+
if (!isset($content[self::REQUEST_KEY_PARTS]) || empty($content[self::REQUEST_KEY_PARTS])) {
185+
$responseJson = json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
186+
$responseStr = is_string($responseJson) ? substr($responseJson, 0, 500) : 'Unable to encode response';
187+
throw new QueryException(
188+
'Invalid response format from Google API: missing or empty "parts" array. Response: ' . $responseStr,
189+
0
190+
);
191+
}
192+
193+
$part = $content[self::REQUEST_KEY_PARTS][0];
194+
195+
if (!isset($part[self::REQUEST_KEY_TEXT])) {
196+
$responseJson = json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
197+
$responseStr = is_string($responseJson) ? substr($responseJson, 0, 500) : 'Unable to encode response';
198+
throw new QueryException(
199+
'Invalid response format from Google API: missing "text" key in part. Response: ' . $responseStr,
200+
0
201+
);
202+
}
203+
204+
return (string)$part[self::REQUEST_KEY_TEXT];
123205
}
124206

125207
protected function buildSystemPrompt(string $type): string

0 commit comments

Comments
 (0)