streamMessage method
override
Stream a response from the AI proxy via SSE.
systemPrompt is ignored — the proxy builds it server-side.
Implementation
@override
Stream<String> streamMessage({
required String systemPrompt,
required List<Map<String, String>> messages,
int maxTokens = 8192,
}) async* {
dev.log(
'Proxy stream request: provider=$providerName, mode=$mode, '
'messages=${messages.length}',
name: 'ProxyAiDatasource',
);
final credential = await credentialProvider();
if (credential == null || credential.isEmpty) {
throw AiApiException(
type: AiApiErrorType.auth,
message: '${backendProvider.displayName} credential not configured.',
);
}
final response = await _dio.post<ResponseBody>(
functionUrl,
options: Options(
headers: headersBuilder(),
responseType: ResponseType.stream,
),
data: {
'provider': providerName,
'credential': credential,
'mode': mode,
if (topicId != null) 'topicId': topicId,
'messages': messages,
'isValidation': isValidation,
if (validationTarget != null) 'validationTarget': validationTarget,
'isCardGeneration': isCardGeneration,
'maxTokens': maxTokens,
'stream': true,
},
);
final stream = response.data?.stream;
if (stream == null) {
throw Exception('No stream body from AI proxy.');
}
// Parse unified SSE: data: {"text": "..."} or data: [DONE]
String buffer = '';
await for (final chunk in stream) {
buffer += utf8.decode(chunk);
while (buffer.contains('\n\n')) {
final idx = buffer.indexOf('\n\n');
final eventBlock = buffer.substring(0, idx);
buffer = buffer.substring(idx + 2);
for (final line in eventBlock.split('\n')) {
if (!line.startsWith('data: ')) continue;
final dataStr = line.substring(6).trim();
if (dataStr == '[DONE]') return;
try {
final json = jsonDecode(dataStr) as Map<String, dynamic>;
final text = json['text'] as String?;
if (text != null) yield text;
} catch (_) {
// Skip malformed SSE lines.
}
}
}
}
}