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:
Alma Alexyz
2026-09-01 22:05:38 +05:00
parent f3e7412cf9
commit fbf04d90db
49 changed files with 4856 additions and 92 deletions
+124
View File
@@ -0,0 +1,124 @@
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';
class Pollinations extends BaseProvider {
@override
String get name => 'Pollinations';
@override
bool get working => true;
@override
List<String> get models => [
'openai-fast',
'openai',
'deepseek',
G4FModels.gpt4o,
G4FModels.mistral,
G4FModels.llama3,
G4FModels.qwen,
];
@override
String get defaultModel => 'openai-fast';
static const _getBase = 'https://text.pollinations.ai';
static const _postUrl = 'https://gen.pollinations.ai/v1/chat/completions';
String _resolveModel(String model) {
switch (model) {
case G4FModels.gpt4o:
case G4FModels.gpt4oMini:
case G4FModels.gpt4:
return 'openai-fast';
case G4FModels.mistral:
case G4FModels.llama3:
case G4FModels.qwen:
return 'openai-fast';
default:
return models.contains(model) ? model : defaultModel;
}
}
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final resolved = _resolveModel(model);
try {
await for (final chunk in _getPrompt(resolved, messages, stream: stream)) {
yield chunk;
}
return;
} catch (_) {
await for (final chunk in _postChat(resolved, messages, stream: stream)) {
yield chunk;
}
}
}
Stream<String> _postChat(
String model,
List<Message> messages, {
required bool stream,
}) async* {
final req = http.Request('POST', Uri.parse(_postUrl));
req.headers.addAll(browserHeaders(
contentType: 'application/json',
accept: stream ? 'text/event-stream' : 'application/json',
));
req.body = jsonEncode({
'model': model,
'messages': toOpenAiMessages(messages),
'stream': false, // anonymous POST rejects SSE
});
final response = await g4fHttp.send(req).timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
final err = await response.stream.bytesToString().timeout(const Duration(seconds: 4));
throw Exception('Pollinations POST ${response.statusCode}: $err');
}
final body = utf8.decode(await response.stream.toBytes().timeout(const Duration(seconds: 30)));
if (looksLikeGarbage(body)) {
throw Exception('Pollinations POST garbage');
}
final text = extractOpenAiMessage(body) ?? body.trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Pollinations POST empty');
}
yield text;
}
Stream<String> _getPrompt(
String model,
List<Message> messages, {
required bool stream,
}) async* {
final seed = DateTime.now().millisecondsSinceEpoch % 100000;
final prompt = formatPrompt(messages);
final url = Uri.parse(
'$_getBase/${Uri.encodeComponent(prompt)}?model=$model&seed=$seed',
);
final req = http.Request('GET', url);
req.headers.addAll(browserHeaders());
final response = await g4fHttp.send(req).timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
throw Exception('Pollinations returned status ${response.statusCode}');
}
final fullText = utf8
.decode(await response.stream.toBytes().timeout(const Duration(seconds: 30)))
.trim();
if (!looksLikeModelOutput(fullText)) {
throw Exception('Pollinations GET empty');
}
yield fullText;
}
}