completeStream method

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

Sends a completion request and returns a stream of chunks.

Implementation

@override
Stream<AIStreamChunk> completeStream(
  List<AIMessage> messages, {
  int? maxTokens,
  double? temperature,
  List<AITool>? tools,
}) async* {
  final body =
      _buildRequestBody(messages, maxTokens, temperature, tools: tools);
  final url =
      '$_baseUrl/models/${config.model}:streamGenerateContent?alt=sse&key=${config.apiKey}';

  final request = http.Request('POST', Uri.parse(url));
  request.headers['Content-Type'] = 'application/json';
  request.body = jsonEncode(body);

  final streamedResponse = await _httpClient.send(request);

  if (streamedResponse.statusCode != 200) {
    final responseBody = await streamedResponse.stream.bytesToString();
    final json = jsonDecode(responseBody) as Map<String, dynamic>;
    throw _parseError(streamedResponse.statusCode, json);
  }

  await for (final chunk in streamedResponse.stream
      .transform(utf8.decoder)
      .transform(const LineSplitter())) {
    if (chunk.isEmpty) continue;
    if (!chunk.startsWith('data: ')) continue;

    final jsonStr = chunk.substring(6);
    try {
      final json = jsonDecode(jsonStr) as Map<String, dynamic>;
      final candidates = json['candidates'] as List? ?? [];
      if (candidates.isEmpty) continue;

      final candidate = candidates.first as Map<String, dynamic>;
      final content = candidate['content'] as Map<String, dynamic>?;
      final parts = (content?['parts'] as List?) ?? [];
      final text = parts
          .map((p) => (p as Map<String, dynamic>)['text'] as String? ?? '')
          .join();

      final finishReason = candidate['finishReason'] as String?;

      AIUsage? usage;
      final usageMetadata = json['usageMetadata'] as Map<String, dynamic>?;
      if (usageMetadata != null) {
        usage = AIUsage(
          promptTokens: usageMetadata['promptTokenCount'] as int? ?? 0,
          completionTokens:
              usageMetadata['candidatesTokenCount'] as int? ?? 0,
        );
      }

      if (text.isNotEmpty) {
        yield AIStreamChunk(
          text: text,
          isComplete: finishReason == 'STOP',
          finishReason: finishReason,
          usage: usage,
          provider: name,
          model: config.model,
        );
      }
    } on FormatException catch (_) {
      // Skip malformed JSON chunks
    }
  }
}