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.
115 lines
3.8 KiB
Dart
115 lines
3.8 KiB
Dart
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');
|
|
}
|
|
}
|
|
}
|