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,98 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../http_client.dart';
|
||||
import '../models.dart';
|
||||
import '../sse.dart';
|
||||
import 'base_provider.dart';
|
||||
|
||||
class ChatGptOss extends BaseProvider {
|
||||
@override
|
||||
String get name => 'ChatGptOss';
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
List<String> get models => ['gpt-oss-120b', 'gpt-5-nano'];
|
||||
|
||||
@override
|
||||
String get defaultModel => 'gpt-oss-120b';
|
||||
|
||||
String _generateFingerprint() {
|
||||
final seed = '${DateTime.now().millisecondsSinceEpoch / 1000.0}-${Random().nextDouble()}';
|
||||
return md5.convert(utf8.encode(seed)).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> createAsyncGenerator(
|
||||
String model,
|
||||
List<Message> messages, {
|
||||
bool stream = false,
|
||||
}) async* {
|
||||
final prompt = formatPrompt(messages);
|
||||
final url = Uri.parse('https://chat-gpt-oss.com/api/message');
|
||||
|
||||
final payload = {
|
||||
"conversation_id": null,
|
||||
"model": model,
|
||||
"content": prompt,
|
||||
"reasoning_effort": "low",
|
||||
};
|
||||
|
||||
final req = http.Request('POST', url);
|
||||
req.headers.addAll({
|
||||
"accept": "text/event-stream",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"cache-control": "no-cache",
|
||||
"content-type": "application/json",
|
||||
"origin": "https://chat-gpt-oss.com",
|
||||
"pragma": "no-cache",
|
||||
"referer": "https://chat-gpt-oss.com/",
|
||||
"sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Linux"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
||||
"x-fingerprint": _generateFingerprint(),
|
||||
});
|
||||
|
||||
req.body = jsonEncode(payload);
|
||||
|
||||
final response = await g4fHttp.send(req);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
String? currentEvent;
|
||||
await for (final line in splitSseLines(response.stream.transform(utf8.decoder))) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
currentEvent = null;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith('event:')) {
|
||||
currentEvent = trimmed.substring(6).trim();
|
||||
if (currentEvent == 'summary') {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith('data:') && currentEvent == 'message') {
|
||||
final dataStr = trimmed.substring(5).trim();
|
||||
if (dataStr.isEmpty) continue;
|
||||
try {
|
||||
final jsonChunk = jsonDecode(dataStr);
|
||||
final content = jsonChunk['content'] as String?;
|
||||
if (content != null && content.isNotEmpty) {
|
||||
yield content;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw Exception('$name returned status ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user