fbf04d90db
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.
125 lines
3.5 KiB
Dart
125 lines
3.5 KiB
Dart
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;
|
|
}
|
|
}
|