sendChatRequest method

Future<Map<String, dynamic>> sendChatRequest(
  1. List<Map<String, dynamic>> messages,
  2. List<Map<String, dynamic>>? tools, {
  3. double? temperature,
  4. int? maxCompletionTokens,
  5. double? topP,
  6. bool? stream,
  7. String? reasoningEffort,
  8. dynamic stop,
  9. CancellationToken? cancellationToken,
})

Sends a chat request to the API.

messages should be a list of message maps with 'role' and 'content'. tools is an optional list of tool definitions. Other parameters override defaults if provided.

Returns the API response as a map. Throws an exception on failure.

Implementation

Future<Map<String, dynamic>> sendChatRequest(
  List<Map<String, dynamic>> messages,
  List<Map<String, dynamic>>? tools, {
  double? temperature,
  int? maxCompletionTokens,
  double? topP,
  bool? stream,
  String? reasoningEffort,
  dynamic stop,
  CancellationToken? cancellationToken,
}) async {
  sdkLogger.info(
    'Sending chat request to API (model: $model)',
    tag: 'API',
    extra: {
      'model': model,
      'message_count': messages.length,
      'has_tools': tools != null && tools.isNotEmpty,
      if (sdkLogger.options.logSensitiveContent) 'messages': messages,
    },
  );

  final headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $apiKey',
  };

  final bodyMap = {
    'model': model,
    'messages': messages,
    'tools': tools,
    'tool_choice': 'auto',
    if ((temperature ?? this.temperature) != null)
      'temperature': temperature ?? this.temperature,
    if ((maxCompletionTokens ?? this.maxCompletionTokens) != null)
      'max_completion_tokens':
          maxCompletionTokens ?? this.maxCompletionTokens,
    if ((topP ?? this.topP) != null) 'top_p': topP ?? this.topP,
    if ((stream ?? this.stream) != null) 'stream': stream ?? this.stream,
    if ((reasoningEffort ?? this.reasoningEffort) != null)
      'reasoning_effort': reasoningEffort ?? this.reasoningEffort,
    if ((stop ?? this.stop) != null) 'stop': stop ?? this.stop,
  };

  final body = jsonEncode(bodyMap);

  const int maxRetries = 3;
  const Duration baseDelay = Duration(seconds: 1);

  for (int attempt = 1; attempt <= maxRetries; attempt++) {
    if (cancellationToken?.isCancelled == true) {
      sdkLogger.info('API request cancelled by user', tag: 'API');
      throw Exception('Request cancelled by user');
    }

    try {
      final stopwatch = Stopwatch()..start();

      sdkLogger.info(
        'Sending API request (model: $model) attempt $attempt',
        tag: 'API',
        extra: {
          'model': model,
          'url': baseUrl,
          'body_length': body.length,
          'headers': {...headers, 'Authorization': 'Bearer [REDACTED]'},
        },
      );

      final response = await _httpClient.post(
        Uri.parse(baseUrl),
        headers: headers,
        body: body,
      );

      stopwatch.stop();
      sdkLogger.logPerformance(
        'API request (model: $model)',
        stopwatch.elapsed,
        context: {'model': model, 'status_code': response.statusCode},
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        if (data['usage'] != null) {
          final usage = data['usage'];
          final promptTokens = usage['prompt_tokens'] ?? 0;
          final completionTokens = usage['completion_tokens'] ?? 0;
          final totalTokens = usage['total_tokens'] ?? 0;

          final cost =
              (promptTokens * 0.00059 / 1000) +
              (completionTokens * 0.00079 / 1000);

          sdkLogger.info(
            'API request successful. Model: ${data['model']}',
            tag: 'API',
            extra: {
              'usage': {
                'prompt_tokens': promptTokens,
                'completion_tokens': completionTokens,
                'total_tokens': totalTokens,
                'estimated_cost_usd': cost.toStringAsFixed(6),
              },
              if (sdkLogger.options.logSensitiveContent) 'response': data,
            },
          );
        }
        return data;
      } else if (response.statusCode == 429) {
        if (attempt == maxRetries) {
          throw Exception(
            'Rate limit exceeded after $maxRetries attempts: ${response.body}',
          );
        }

        final retryAfterContent = response.headers['retry-after'];
        int retrySeconds = attempt * 2;

        if (retryAfterContent != null) {
          retrySeconds = int.tryParse(retryAfterContent) ?? retrySeconds;
        }

        sdkLogger.warning(
          'Rate limit hit (429). Retrying in ${retrySeconds}s... (Attempt $attempt/$maxRetries)',
          tag: 'API',
          extra: {'response_redacted': '[REDACTED]'},
        );

        await Future.delayed(Duration(seconds: retrySeconds));
        continue;
      } else {
        sdkLogger.error(
          'API request (model: $model) failed',
          tag: 'API',
          extra: {
            'status_code': response.statusCode,
            if (sdkLogger.options.logSensitiveContent)
              'response_body': response.body,
          },
        );
        throw Exception('Failed to get response: ${response.statusCode}');
      }
    } on http.ClientException catch (e) {
      if (attempt == maxRetries) {
        sdkLogger.error(
          'API request (model: $model) failed after $maxRetries attempts',
          tag: 'API',
          error: e,
        );
        rethrow;
      }
      sdkLogger.warning(
        'API request (model: $model) attempt $attempt failed, retrying...',
        tag: 'API',
        error: e,
      );
      await Future.delayed(baseDelay * attempt);
    } catch (e, stackTrace) {
      sdkLogger.error(
        'API request (model: $model) error',
        tag: 'API',
        error: e,
        stackTrace: stackTrace,
      );
      rethrow;
    }
  }

  throw Exception('Unexpected error in API request');
}