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,114 @@
|
||||
import 'dart:convert';
|
||||
import '../http_util.dart';
|
||||
import '../models.dart';
|
||||
import '../sse.dart';
|
||||
import 'webview_base_provider.dart';
|
||||
|
||||
/// HuggingChat (huggingface.co/chat). Guest REST is blocked; WebView carries cookies.
|
||||
class HuggingChat extends WebViewBaseProvider {
|
||||
@override
|
||||
String get name => 'HuggingChat';
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
List<String> get models => [
|
||||
'omni',
|
||||
'meta-llama/Llama-3.1-8B-Instruct',
|
||||
'Qwen/Qwen3.8-27B',
|
||||
'zai-org/GLM-5.3-Flash',
|
||||
];
|
||||
|
||||
@override
|
||||
String get defaultModel => 'omni';
|
||||
|
||||
@override
|
||||
String get baseUrl => 'https://huggingface.co/chat/';
|
||||
|
||||
@override
|
||||
String get apiEndpoint => 'https://huggingface.co/chat/api/v2/conversations';
|
||||
|
||||
@override
|
||||
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
|
||||
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
|
||||
final promptJson = jsonEncode(formatPrompt(messages));
|
||||
return '''
|
||||
(async function() {
|
||||
const headers = {'Content-Type': 'application/json', 'Accept': 'application/json'};
|
||||
let convRes = await fetch('/chat/api/v2/conversations', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ model: $modelJson })
|
||||
});
|
||||
if (!convRes.ok) {
|
||||
convRes = await fetch('/chat/conversation', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ model: $modelJson })
|
||||
});
|
||||
}
|
||||
if (!convRes.ok) {
|
||||
return JSON.stringify({error: 'HuggingChat login required: ' + convRes.status + ' ' + await convRes.text()});
|
||||
}
|
||||
const conv = await convRes.json();
|
||||
const id = conv.json?.id || conv.id || conv.conversationId;
|
||||
if (!id) return JSON.stringify({error: 'no conversation id', raw: conv});
|
||||
|
||||
const payload = {
|
||||
id: crypto.randomUUID(),
|
||||
inputs: $promptJson,
|
||||
is_retry: false,
|
||||
is_continue: false,
|
||||
web_search: false,
|
||||
tools: []
|
||||
};
|
||||
let msgRes = await fetch('/chat/api/v2/conversations/' + id, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!msgRes.ok) {
|
||||
msgRes = await fetch('/chat/conversation/' + id, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
const body = await msgRes.text();
|
||||
return JSON.stringify({status: msgRes.status, body});
|
||||
})();
|
||||
''';
|
||||
}
|
||||
|
||||
@override
|
||||
String parseResponse(String rawResponse) {
|
||||
try {
|
||||
final json = jsonDecode(rawResponse);
|
||||
if (json is Map && json['error'] != null) {
|
||||
throw Exception(json['error']);
|
||||
}
|
||||
final body = json is Map ? (json['body']?.toString() ?? rawResponse) : rawResponse;
|
||||
final sse = extractAllSseContent(body).join();
|
||||
if (looksLikeModelOutput(sse)) return sse;
|
||||
// chat-ui tokens: {"type":"stream","token":"..."}
|
||||
final tokens = <String>[];
|
||||
for (final line in body.split('\n')) {
|
||||
try {
|
||||
final m = jsonDecode(line);
|
||||
if (m is Map) {
|
||||
final token = m['token'] ?? m['text'] ?? m['generated_text'];
|
||||
if (token is String && token.isNotEmpty) tokens.add(token);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
final joined = tokens.join();
|
||||
if (looksLikeModelOutput(joined)) return joined;
|
||||
if (looksLikeModelOutput(body)) return body.trim();
|
||||
throw Exception('HuggingChat unusable: ${body.substring(0, body.length.clamp(0, 180))}');
|
||||
} catch (e) {
|
||||
if (looksLikeModelOutput(rawResponse)) return rawResponse.trim();
|
||||
throw Exception('Failed to parse HuggingChat: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user