streamMessage method

  1. @override
Stream<String> streamMessage({
  1. required String systemPrompt,
  2. required List<Map<String, String>> messages,
  3. int maxTokens = 8192,
})
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.
        }
      }
    }
  }
}