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 request = http.Request('POST', _endpoint)
..headers['Authorization'] = 'Bearer $apiKey'
..headers['Content-Type'] = 'application/json'
..headers['Accept'] = 'text/event-stream'
..body = jsonEncode({
'model': model,
'stream': true,
'messages': _toOpenAiMessages(conversation),
if (tools != null) 'tools': tools,
...?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);
}
final pendingCalls = <int, _PendingToolCall>{};
await for (final payload in sseDataEvents(response.stream)) {
if (payload == '[DONE]') break;
if (payload.isEmpty) continue;
final Map<String, Object?> json;
try {
json = jsonDecode(payload) as Map<String, Object?>;
} on FormatException {
// OpenRouter occasionally sends non-JSON keep-alive comments; skip
// rather than aborting the whole stream.
continue;
}
final choices = json['choices'] as List<Object?>?;
final delta = choices == null || choices.isEmpty
? null
: (choices.first as Map<String, Object?>)['delta']
as Map<String, Object?>?;
if (delta == null) continue;
final content = delta['content'] as String?;
if (content != null && content.isNotEmpty) {
yield TextDelta(content);
}
final toolCalls = delta['tool_calls'] as List<Object?>?;
if (toolCalls != null) {
for (final raw in toolCalls) {
final entry = raw as Map<String, Object?>;
final index = entry['index'] as int? ?? 0;
final function = entry['function'] as Map<String, Object?>?;
final pending = pendingCalls.putIfAbsent(index, _PendingToolCall.new);
final id = entry['id'] as String?;
if (id != null) pending.id = id;
final name = function?['name'] as String?;
if (name != null) pending.name = name;
final argsChunk = function?['arguments'] as String?;
if (argsChunk != null) pending.arguments.write(argsChunk);
}
}
}
for (final pending in pendingCalls.values) {
final call = pending.toToolCall();
if (call != null) yield ToolCallEvent(call);
}
}