import 'dart:convert'; import 'package:http/http.dart' as http; import '../http_client.dart'; import '../http_util.dart'; import '../models.dart'; import '../sse.dart'; import 'base_provider.dart'; /// OpenAI-compatible `/v1/chat/completions` client used by several free proxies. class OpenAiCompatProvider extends BaseProvider { OpenAiCompatProvider({ required this.providerName, required this.apiBase, required this.fallbackModel, List? supportedModels, this.apiKey, this.extraHeaders = const {}, }) : supportedModels = supportedModels ?? [fallbackModel]; final String providerName; final String apiBase; final String fallbackModel; final List supportedModels; final String? apiKey; final Map extraHeaders; @override String get name => providerName; @override bool get working => true; @override List get models => supportedModels; @override String get defaultModel => fallbackModel; String get chatUrl { final base = apiBase.endsWith('/') ? apiBase.substring(0, apiBase.length - 1) : apiBase; if (base.endsWith('/chat/completions')) return base; return '$base/chat/completions'; } @override Stream createAsyncGenerator( String model, List messages, { bool stream = false, }) async* { final payload = { 'model': models.contains(model) ? model : defaultModel, 'messages': toOpenAiMessages(messages), 'stream': false, 'temperature': 0.8, }; final headers = browserHeaders( contentType: 'application/json', accept: 'application/json', extra: { if (apiKey != null && apiKey!.isNotEmpty) 'Authorization': 'Bearer $apiKey', ...extraHeaders, }, ); final req = http.Request('POST', Uri.parse(chatUrl)); req.headers.addAll(headers); req.body = jsonEncode(payload); final response = await g4fHttp.send(req).timeout(requestTimeout); if (response.statusCode < 200 || response.statusCode >= 300) { final err = await response.stream.bytesToString(); throw Exception( '$name returned ${response.statusCode}: ${err.substring(0, err.length.clamp(0, 240))}', ); } final body = utf8.decode(await response.stream.toBytes()); final fromJson = extractOpenAiMessage(body); final fromSse = extractAllSseContent(body).join(); final text = (fromJson != null && looksLikeModelOutput(fromJson)) ? fromJson : fromSse; if (!looksLikeModelOutput(text)) { throw Exception('$name returned unusable body'); } yield text; } }