streamEvents method
Streams structured events for conversation's next assistant turn.
conversation is oldest-first; its last element is the new user turn.
Implementation
@override
Stream<ProviderEvent> streamEvents(List<ChatMessage> conversation) async* {
final systemText = conversation
.where((m) => m.role == ChatRole.system)
.map((m) => m.text)
.where((t) => t.isNotEmpty)
.join('\n\n');
final request = http.Request('POST', _endpoint)
..headers['Content-Type'] = 'application/json'
..headers['Accept'] = 'text/event-stream'
..body = jsonEncode({
if (systemText.isNotEmpty)
'systemInstruction': {
'parts': [
{'text': systemText},
],
},
'contents': _toGeminiContents(conversation),
if (functionDeclarations != null)
'tools': [
{'functionDeclarations': functionDeclarations},
],
...?extraParams,
});
final response = await _httpClient.send(request);
if (response.statusCode < 200 || response.statusCode >= 300) {
final body = await response.stream.bytesToString();
throw AgentProviderException(response.statusCode, body);
}
var callCounter = 0;
// No [DONE] sentinel here, unlike OpenRouter -- the SSE stream simply
// ends when the HTTP response completes, and sseDataEvents' async*
// terminates naturally at that point.
await for (final payload in sseDataEvents(response.stream)) {
if (payload.isEmpty) continue;
final Map<String, Object?> json;
try {
json = jsonDecode(payload) as Map<String, Object?>;
} on FormatException {
continue;
}
final candidates = json['candidates'] as List<Object?>?;
if (candidates == null || candidates.isEmpty) continue;
final content =
(candidates.first as Map<String, Object?>)['content']
as Map<String, Object?>?;
final parts = content?['parts'] as List<Object?>?;
if (parts == null) continue;
for (final rawPart in parts) {
final part = rawPart as Map<String, Object?>;
final text = part['text'] as String?;
if (text != null && text.isNotEmpty) {
yield TextDelta(text);
}
final functionCall = part['functionCall'] as Map<String, Object?>?;
if (functionCall != null) {
final name = functionCall['name'] as String?;
if (name == null) continue;
final args = functionCall['args'];
final input = args == null
? '{}'
: const JsonEncoder.withIndent(' ').convert(args);
callCounter++;
yield ToolCallEvent(
ToolCall(
// Gemini's functionCall carries no id -- synthesize one, the
// same no-uuid-dependency approach ChatMessage's own id
// generator uses.
id: '${DateTime.now().microsecondsSinceEpoch}-fc-$callCounter',
name: name,
status: ToolCallStatus.pending,
input: input,
),
);
}
}
}
}