stream method

  1. @override
Future<Stream<StreamingMessage>> stream(
  1. List<LLMMessage> messages, {
  2. List<Tool>? tools,
  3. ToolChoice? toolChoice,
  4. required ModelConfig modelConfig,
  5. bool? jsonOutput,
  6. CancelToken? cancelToken,
})
override

Implementation

@override
Future<Stream<StreamingMessage>> stream(
  List<LLMMessage> messages, {
  List<Tool>? tools,
  ToolChoice? toolChoice,
  required ModelConfig modelConfig,
  bool? jsonOutput,
  CancelToken? cancelToken,
}) async {
  final url = '$baseUrl/responses';
  final body = _createRequestBody(
    messages,
    tools: tools,
    toolChoice: toolChoice,
    modelConfig: modelConfig,
    stream: true,
    jsonOutput: jsonOutput,
    autoPreviousResponseId: autoPreviousResponseId,
    extraAllowedKeys: extraAllowedKeys,
  );

  StreamController<StreamingMessage> controller =
      StreamController<StreamingMessage>();
  int retryCount = 0;
  int currentDelayMs = initialRetryDelayMs;

  Future<void> waitForRetry(String reason) async {
    _logger.warning(
      'Responses API Stream: $reason. Retrying in ${currentDelayMs}ms... (Attempt ${retryCount + 1}/$maxRetries)',
    );
    await Future.delayed(Duration(milliseconds: currentDelayMs));
    retryCount++;
    currentDelayMs = (currentDelayMs * 2);
    if (currentDelayMs > maxRetryDelayMs) {
      currentDelayMs = maxRetryDelayMs;
    }
  }

  void pumpStream() async {
    while (true) {
      try {
        _logger.info(
          'Sending streaming request to OpenAI Responses API, baseUrl: $baseUrl, timeout: ${timeout.inSeconds} seconds, proxy:${proxyUrl ?? 'none'}, message length: ${messages.length}, tools: ${tools?.length}, model: ${modelConfig.model}',
        );
        final startTime = DateTime.now();

        final response = await _client.post(
          url,
          data: body,
          options: Options(
            responseType: ResponseType.stream,
            sendTimeout: timeout,
            receiveTimeout: timeout,
            headers: {
              'Content-Type': 'application/json',
              'Authorization': 'Bearer $_apiKey',
            },
            validateStatus: (code) => true,
          ),
          cancelToken: cancelToken,
        );
        final endTime = DateTime.now();

        _logger.info(
          'Received streaming response from OpenAI Responses API, status code: ${response.statusCode}, duration: ${endTime.difference(startTime).inMilliseconds} ms',
        );

        if (response.statusCode != 200) {
          if (response.statusCode != null &&
              (response.statusCode == 429 || response.statusCode! >= 500)) {
            if (retryCount < maxRetries) {
              await waitForRetry(
                'Returned status code ${response.statusCode}',
              );
              controller.add(
                StreamingMessage(
                  controlMessage: StreamingControlMessage(
                    controlFlag: StreamingControlFlag.retry,
                    data: {
                      'retryReason':
                          'Returned status code ${response.statusCode}',
                    },
                  ),
                ),
              );
              continue;
            }
          }

          final responseBody = await utf8.decodeStream(
            (response.data.stream as Stream).cast<List<int>>(),
          );
          throw Exception(
            'Failed to stream from OpenAI Responses API: ${response.statusCode} ${response.statusMessage} $responseBody',
          );
        }

        final stream = (response.data.stream as Stream).cast<List<int>>();

        final transformedStream = stream
            .transform(utf8.decoder)
            .transform(const LineSplitter())
            .transform(ResponsesChunkDecoder()) // Reusing SSE decoder
            .transform(ResponsesAPIResponseTransformer(modelConfig))
            .map((chunk) => StreamingMessage(modelMessage: chunk));

        await for (final message in transformedStream) {
          controller.add(message);
        }

        controller.close();
        break;
      } on DioException catch (e) {
        if (isLlmRequestCancelled(e)) {
          controller.addError(e);
          controller.close();
          break;
        }
        if (retryCount < maxRetries) {
          await waitForRetry('DioException: ${e.message}');
          controller.add(
            StreamingMessage(
              controlMessage: StreamingControlMessage(
                controlFlag: StreamingControlFlag.retry,
                data: {'retryReason': 'DioException: ${e.message}'},
              ),
            ),
          );
          continue;
        }
        controller.addError(e);
        controller.close();
        break;
      } catch (e) {
        controller.addError(e);
        controller.close();
        break;
      }
    }
  }

  pumpStream();
  return controller.stream;
}