postSseData function
JSON ボディを POST し、SSE(Server-Sent Events)の data ペイロードを
イベント単位で流す。
SSE の仕様に従い、複数行の data: は改行で連結し、空行でイベントを
区切る。コメント行(: 始まり)や event: / id: などのフィールドは
無視する(各プロバイダーの data JSON はイベント種別を自身に含むため)。
エラーは requestJson と同じ階層に正規化する: 通信失敗・切断は LLMNetworkException、非 2xx は LLMException.fromHttp。
Implementation
Stream<String> postSseData({
required String url,
required String provider,
required Map<String, String> headers,
required Map<String, dynamic> body,
Map<String, String>? queryParameters,
}) async* {
var uri = Uri.parse(url);
if (queryParameters != null && queryParameters.isNotEmpty) {
uri = uri.replace(
queryParameters: {...uri.queryParameters, ...queryParameters},
);
}
final request = http.Request('POST', uri)
..headers.addAll({
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
...headers,
})
..body = jsonEncode(body);
final client = http.Client();
try {
http.StreamedResponse response;
try {
response = await client.send(request);
} catch (e) {
throw LLMNetworkException(
'Network request failed',
provider: provider,
raw: e,
);
}
if (response.statusCode < 200 || response.statusCode >= 300) {
throw LLMException.fromHttp(
response.statusCode,
provider: provider,
body: await _readErrorBody(response),
);
}
final lines =
response.stream.transform(utf8.decoder).transform(const LineSplitter());
final dataBuffer = StringBuffer();
var hasData = false;
try {
await for (final line in lines) {
if (line.isEmpty) {
// 空行 = イベントの区切り。
if (hasData) {
yield dataBuffer.toString();
dataBuffer.clear();
hasData = false;
}
continue;
}
if (line.startsWith(':')) continue; // コメント行。
if (line.startsWith('data:')) {
var value = line.substring(5);
if (value.startsWith(' ')) value = value.substring(1);
if (hasData) dataBuffer.write('\n');
dataBuffer.write(value);
hasData = true;
}
}
} on LLMException {
rethrow;
} catch (e) {
throw LLMNetworkException(
'Connection lost during streaming',
provider: provider,
raw: e,
);
}
if (hasData) yield dataBuffer.toString();
} finally {
client.close();
}
}