postSseData function

Stream<String> postSseData({
  1. required String url,
  2. required String provider,
  3. required Map<String, String> headers,
  4. required Map<String, dynamic> body,
  5. Map<String, String>? queryParameters,
})

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();
  }
}