streamChat method

  1. @override
Stream<Object> streamChat(
  1. List<AiChatMessage> messages, {
  2. required String model,
  3. double? temperature,
  4. int? maxTokens,
  5. Map<String, Object?> extraBody = const {},
})
override

Streams a chat completion as a sequence of typed events.

Implementation

@override
Stream<Object> streamChat(
  List<AiChatMessage> messages, {
  required String model,
  double? temperature,
  int? maxTokens,
  Map<String, Object?> extraBody = const {},
}) async* {
  // Anthropic takes the system prompt as a top-level parameter, not a
  // message in the array.
  final systemPrompt = messages
      .where((m) => m.role == AiRole.system)
      .map((m) => m.content)
      .join('\n');
  final chat = messages.where((m) => m.role != AiRole.system).toList();

  final uri = Uri.parse('$baseUrl/messages');
  final body = jsonEncode({
    'model': model,
    'max_tokens': maxTokens ?? 4096, // required by Anthropic
    'messages': [for (final m in chat) m.toJson()],
    'stream': true,
    if (systemPrompt.isNotEmpty) 'system': systemPrompt,
    ?temperature: temperature,
    ...extraBody,
  });

  final request = http.Request('POST', uri)
    ..headers['Content-Type'] = 'application/json'
    ..headers['Accept'] = 'text/event-stream'
    ..headers['x-api-key'] = apiKey
    ..headers['anthropic-version'] = version
    ..body = body;

  final response = await _client.send(request);
  if (response.statusCode != 200) {
    final errorBody = await response.stream.bytesToString();
    yield AiError(
      'Request failed with HTTP ${response.statusCode}: $errorBody',
      statusCode: response.statusCode,
    );
    return;
  }

  final parser = SseParser();
  final state = _AnthropicStreamState();

  await for (final chunk in response.stream.transform(
    const Utf8Decoder(allowMalformed: true),
  )) {
    for (final sseEvent in parser.addChunk(chunk)) {
      for (final event in state.consume(sseEvent)) {
        if (event is _DoneSignal) {
          yield AiDone(
            finishReason: event.finishReason ?? 'end_turn',
            usage: AiUsage(
              inputTokens: state.inputTokens,
              outputTokens: state.outputTokens,
            ),
          );
          return;
        }
        yield event;
      }
    }
  }

  yield AiDone(
    finishReason: 'end_turn',
    usage: AiUsage(
      inputTokens: state.inputTokens,
      outputTokens: state.outputTokens,
    ),
  );
}