complete method
Sends a completion request and returns the full response.
Implementation
@override
Future<AIResponse> complete(
List<AIMessage> messages, {
int? maxTokens,
double? temperature,
List<AITool>? tools,
}) async {
final stopwatch = Stopwatch()..start();
final body =
_buildRequestBody(messages, maxTokens, temperature, tools: tools);
final url =
'$_baseUrl/models/${config.model}:generateContent?key=${config.apiKey}';
final response = await _httpClient
.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
)
.timeout(config.timeout);
stopwatch.stop();
final json = jsonDecode(response.body) as Map<String, dynamic>;
if (response.statusCode != 200) {
throw _parseError(response.statusCode, json);
}
final candidates = json['candidates'] as List? ?? [];
if (candidates.isEmpty) {
throw AIContentFilterError(
provider: name,
message: 'No candidates returned — content may have been filtered',
);
}
final candidate = candidates.first as Map<String, dynamic>;
final content = candidate['content'] as Map<String, dynamic>?;
final parts = (content?['parts'] as List?) ?? [];
String text = '';
List<AIToolCall>? toolCalls;
for (final p in parts) {
if (p is Map<String, dynamic>) {
if (p.containsKey('text')) {
text += p['text'] as String;
} else if (p.containsKey('functionCall')) {
final fc = p['functionCall'] as Map<String, dynamic>;
toolCalls ??= [];
toolCalls.add(AIToolCall(
id: fc['name'] as String, // Gemini uses name as the identifier
name: fc['name'] as String,
arguments: fc['args'] as Map<String, dynamic>? ?? {},
));
}
}
}
final usageMetadata = json['usageMetadata'] as Map<String, dynamic>? ?? {};
return AIResponse(
content: text,
usage: AIUsage(
promptTokens: usageMetadata['promptTokenCount'] as int? ?? 0,
completionTokens: usageMetadata['candidatesTokenCount'] as int? ?? 0,
estimatedCostUsd: _estimateCost(
usageMetadata['promptTokenCount'] as int? ?? 0,
usageMetadata['candidatesTokenCount'] as int? ?? 0,
),
),
model: config.model,
provider: name,
latency: stopwatch.elapsed,
finishReason: candidate['finishReason'] as String?,
toolCalls: toolCalls,
);
}