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,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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user