streamChat method
Stream<Object>
streamChat(
- List<
AiChatMessage> messages, { - required String model,
- double? temperature,
- int? maxTokens,
- Map<
String, Object?> extraBody = const {},
override
Streams a chat completion as a sequence of typed events.
Emits AiTextDelta for each content chunk, AiToolCall when the model invokes tools, and terminates with AiDone. Errors surface as AiError (the stream does not throw — UI code can just switch on event types).
Implementation
@override
Stream<Object> streamChat(
List<AiChatMessage> messages, {
required String model,
double? temperature,
int? maxTokens,
Map<String, Object?> extraBody = const {},
}) async* {
final uri = Uri.parse('$baseUrl/chat/completions');
final body = jsonEncode({
'model': model,
'messages': [for (final m in messages) m.toJson()],
'stream': true,
?temperature: temperature,
?maxTokens: maxTokens,
...extraBody,
});
final request = http.Request('POST', uri)
..headers['Content-Type'] = 'application/json'
..headers['Accept'] = 'text/event-stream';
if (apiKey != null && apiKey!.isNotEmpty) {
request.headers['Authorization'] = 'Bearer $apiKey';
}
_extraHeaders.forEach((name, value) {
request.headers[name] = value;
});
request.body = body;
final response = await _client.send(request);
if (response.statusCode != 200) {
final errorBody = await response.stream.bytesToString();
yield AiError(
'Request failed with HTTP ${response.statusCode}: $errorBody',
statusCode: response.statusCode,
);
return;
}
final parser = SseParser();
final usageCollector = _UsageCollector();
final toolAssembler = _ToolCallAssembler();
await for (final chunk in response.stream.transform(
const Utf8Decoder(allowMalformed: true),
)) {
for (final event in parser.addChunk(chunk)) {
for (final parsed in _parseEvent(
event,
usageCollector,
toolAssembler,
)) {
if (parsed is _DoneSignal) {
// Tool-call fragments are complete once the stream signals a
// finish reason — emit the assembled calls before terminating.
for (final call in toolAssembler.flush()) {
yield call;
}
yield AiDone(
finishReason: parsed.finishReason ?? 'stop',
usage: usageCollector.snapshot(),
);
return;
}
yield parsed;
}
}
}
// Stream ended without [DONE] — still terminate cleanly.
for (final call in toolAssembler.flush()) {
yield call;
}
yield AiDone(finishReason: 'stop', usage: usageCollector.snapshot());
}