feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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<String>? supportedModels,
|
||||
this.apiKey,
|
||||
this.extraHeaders = const {},
|
||||
}) : supportedModels = supportedModels ?? [fallbackModel];
|
||||
|
||||
final String providerName;
|
||||
final String apiBase;
|
||||
final String fallbackModel;
|
||||
final List<String> supportedModels;
|
||||
final String? apiKey;
|
||||
final Map<String, String> extraHeaders;
|
||||
|
||||
@override
|
||||
String get name => providerName;
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
List<String> 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<String> createAsyncGenerator(
|
||||
String model,
|
||||
List<Message> messages, {
|
||||
bool stream = false,
|
||||
}) async* {
|
||||
final payload = <String, dynamic>{
|
||||
'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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user