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 'package:http/http.dart' as http;
|
||||
import '../http_client.dart';
|
||||
import '../http_util.dart';
|
||||
import '../models.dart';
|
||||
import 'base_provider.dart';
|
||||
|
||||
/// Port of g4f HuggingSpace / CohereForAI C4AI Command (HF Space).
|
||||
class CohereSpace extends BaseProvider {
|
||||
@override
|
||||
String get name => 'CohereSpace';
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
List<String> get models => [
|
||||
'command-a-03-2025',
|
||||
'command-r-plus-08-2024',
|
||||
'command-r-08-2024',
|
||||
];
|
||||
|
||||
@override
|
||||
String get defaultModel => 'command-a-03-2025';
|
||||
|
||||
static const _url = 'https://coherelabs-c4ai-command.hf.space';
|
||||
|
||||
@override
|
||||
Stream<String> createAsyncGenerator(
|
||||
String model,
|
||||
List<Message> messages, {
|
||||
bool stream = false,
|
||||
}) async* {
|
||||
final useModel = models.contains(model) ? model : defaultModel;
|
||||
final headers = browserHeaders(
|
||||
origin: _url,
|
||||
referer: '$_url/',
|
||||
contentType: 'application/json',
|
||||
);
|
||||
final conv = await g4fHttp
|
||||
.post(
|
||||
Uri.parse('$_url/conversation'),
|
||||
headers: headers,
|
||||
body: jsonEncode({
|
||||
'model': useModel,
|
||||
'preprompt': extractSystemMessage(messages),
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
if (conv.statusCode != 200) {
|
||||
throw Exception('CohereSpace conv ${conv.statusCode}');
|
||||
}
|
||||
final convJson = jsonDecode(conv.body);
|
||||
final convId = (convJson is Map
|
||||
? (convJson['conversationId'] ?? convJson['id'])
|
||||
: null)
|
||||
?.toString();
|
||||
if (convId == null) throw Exception('CohereSpace no conversation');
|
||||
|
||||
final cookie = cookieHeaderFromSetCookie(conv.headers['set-cookie']);
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_url/conversation/$convId'));
|
||||
req.headers.addAll({
|
||||
...browserHeaders(origin: _url, referer: '$_url/'),
|
||||
if (cookie != null) 'Cookie': cookie,
|
||||
});
|
||||
req.fields['data'] = jsonEncode({
|
||||
'inputs': formatPrompt(messages.where((m) => m.role != Role.system).toList()),
|
||||
'id': 0,
|
||||
'is_retry': false,
|
||||
'is_continue': false,
|
||||
'web_search': false,
|
||||
'tools': [],
|
||||
});
|
||||
|
||||
final res = await g4fHttp.send(req).timeout(requestTimeout);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('CohereSpace chat ${res.statusCode}');
|
||||
}
|
||||
final raw = utf8.decode(await res.stream.toBytes().timeout(requestTimeout));
|
||||
final buf = StringBuffer();
|
||||
for (final line in raw.split(RegExp(r'\r?\n'))) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
try {
|
||||
final data = jsonDecode(line);
|
||||
if (data is Map) {
|
||||
if (data['type'] == 'stream' && data['token'] is String) {
|
||||
buf.write((data['token'] as String).replaceAll('\u0000', ''));
|
||||
} else if (data['type'] == 'finalAnswer' && data['text'] is String) {
|
||||
if (buf.isEmpty) buf.write(data['text']);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
final text = buf.toString().trim();
|
||||
if (!looksLikeModelOutput(text)) throw Exception('CohereSpace empty');
|
||||
yield text;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user