complete method

Future<Map<String, dynamic>> complete(
  1. String instructions,
  2. Map<String, dynamic> input
)

Implementation

Future<Map<String, dynamic>> complete(
  String instructions,
  Map<String, dynamic> input,
) async {
  final payload = jsonEncode(input);
  final uri = switch (provider) {
    AiProvider.openai => Uri.https('api.openai.com', '/v1/responses'),
    AiProvider.claude => Uri.https('api.anthropic.com', '/v1/messages'),
    AiProvider.gemini => Uri.https(
      'generativelanguage.googleapis.com',
      '/v1beta/models/$model:generateContent',
    ),
  };
  final body = switch (provider) {
    AiProvider.openai => {
      'model': model,
      'store': false,
      'instructions': instructions,
      'input': payload,
      'max_output_tokens': 8192,
      'text': {
        'format': {'type': 'json_object'},
      },
    },
    AiProvider.claude => {
      'model': model,
      'max_tokens': 8192,
      'system': instructions,
      'messages': [
        {'role': 'user', 'content': payload},
      ],
    },
    AiProvider.gemini => {
      'systemInstruction': {
        'parts': [
          {'text': instructions},
        ],
      },
      'contents': [
        {
          'role': 'user',
          'parts': [
            {'text': payload},
          ],
        },
      ],
      'generationConfig': {
        'responseMimeType': 'application/json',
        'maxOutputTokens': 8192,
      },
    },
  };
  HttpClientRequest? request;
  var expired = false;
  Future<Map<String, dynamic>> send() async {
    final outgoing = await _http.postUrl(uri);
    request = outgoing;
    if (expired) {
      outgoing.abort();
      throw AiException('Request timed out.');
    }
    outgoing.followRedirects = false;
    outgoing.headers.contentType = ContentType.json;
    if (provider == AiProvider.gemini) {
      outgoing.headers.set('x-goog-api-key', _apiKey);
    } else {
      outgoing.headers.set('authorization', 'Bearer $_apiKey');
    }
    if (provider == AiProvider.claude) {
      outgoing.headers.set('anthropic-version', '2026-10-21');
    }
    outgoing.write(jsonEncode(body));
    final response = await outgoing.close();
    if (response.statusCode != 200) {
      throw AiException(
        '${provider.name} returned HTTP ${response.statusCode}. Check the key, model access, quota, and provider status. No automatic retry was made.',
      );
    }
    final bytes = <int>[];
    await for (final chunk in response) {
      if (bytes.length + chunk.length > 1024 * 1024) {
        throw AiException('Provider response exceeded the 1 MiB limit.');
      }
      bytes.addAll(chunk);
    }
    final envelope = jsonDecode(utf8.decode(bytes));
    if (envelope is! Map<String, dynamic>) throw const FormatException();
    final text = _responseText(envelope);
    final decoded = jsonDecode(text);
    if (decoded is! Map<String, dynamic>) throw const FormatException();
    return decoded;
  }

  try {
    return await send().timeout(
      timeout,
      onTimeout: () {
        expired = true;
        request?.abort();
        throw AiException(
          '${provider.name} request timed out; no automatic retry was made.',
        );
      },
    );
  } on AiException {
    rethrow;
  } on IOException {
    throw AiException(
      'Could not reach ${provider.name}. Check your network connection.',
    );
  } catch (_) {
    throw AiException(
      '${provider.name} returned an invalid JSON response. No response content was logged.',
    );
  } finally {
    request?.abort();
  }
}