complete method

  1. @override
Future<AIResponse> complete(
  1. List<AIMessage> messages, {
  2. int? maxTokens,
  3. double? temperature,
  4. List<AITool>? tools,
})

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,
  );
}