Files
999/lib/g4f/providers/opera_aria.dart
T
Alma Alexyz fbf04d90db 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.
2026-09-01 22:05:38 +05:00

138 lines
4.7 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';
/// Port of g4f.Provider.OperaAria — anonymous OAuth, collect SSE to one reply.
class OperaAria extends BaseProvider {
@override
String get name => 'OperaAria';
@override
bool get working => true;
@override
List<String> get models => ['aria'];
@override
String get defaultModel => 'aria';
static const _tokenUrl = 'https://oauth2.opera-api.com/oauth2/v1/token/';
static const _signupUrl =
'https://auth.opera.com/account/v2/external/anonymous/signup';
static const _chatUrl = 'https://composer.opera-api.com/api/v2/a-chat';
static const _secret =
'Pcc5NvlCrxl02pMw32kO6WrnhpS0pUZ95YrDP8XNKJJQvFht4wQDkFJ7v9x5hn7C';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final tokenHeaders = {
'User-Agent': 'okhttp/5.3.2',
'Content-Type': 'application/x-www-form-urlencoded',
'x-requested-with': 'XMLHttpRequest',
'x-opera-client-cache': '1',
};
final anon = await g4fHttp.post(
Uri.parse(_tokenUrl),
headers: tokenHeaders,
body: {
'client_id': 'mini-client',
'client_secret': _secret,
'grant_type': 'client_credentials',
'scope': 'anonymous_account',
},
).timeout(const Duration(seconds: 12));
if (anon.statusCode != 200) {
throw Exception('Opera anon ${anon.statusCode}');
}
final anonToken = jsonDecode(anon.body)['access_token'] as String;
final signup = await g4fHttp.post(
Uri.parse(_signupUrl),
headers: {
'User-Agent': 'okhttp/5.3.2',
'Authorization': 'Bearer $anonToken',
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8',
'x-requested-with': 'XMLHttpRequest',
'x-opera-client-cache': '1',
},
body: jsonEncode({'client_id': 'mini'}),
).timeout(const Duration(seconds: 12));
if (signup.statusCode != 200) {
throw Exception('Opera signup ${signup.statusCode}');
}
final authToken = jsonDecode(signup.body)['token'] as String;
final refresh = await g4fHttp.post(
Uri.parse(_tokenUrl),
headers: tokenHeaders,
body: {
'auth_token': authToken,
'client_id': 'mini',
'grant_type': 'auth_token',
'scope': 'shodan:aria',
},
).timeout(const Duration(seconds: 12));
if (refresh.statusCode != 200) {
throw Exception('Opera refresh ${refresh.statusCode}');
}
final access = jsonDecode(refresh.body)['access_token'] as String;
final req = http.Request('POST', Uri.parse(_chatUrl));
req.headers.addAll({
'Accept': 'text/event-stream',
'Authorization': 'Bearer $access',
'Content-Type': 'application/json',
'Origin': 'https://composer.opera-api.com',
'Referer': 'https://composer.opera-api.com/assets/aria/index.html',
'User-Agent':
'Mozilla/5.0 (Linux; U; Android 14; Pixel 8 Pro Build/UQ1A.240205.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.179 Mobile Safari/537.36 OPR/99.0.2254.81922',
'X-Opera-Timezone': '+03:00',
'X-Opera-UI-Language': 'en',
'X-Requested-With': 'com.opera.mini.native',
});
req.body = jsonEncode({
'query': formatPrompt(messages),
'sia': true,
'think_harder': false,
'supported_features': [],
'file_attachments': [],
});
final res = await g4fHttp.send(req).timeout(requestTimeout);
if (res.statusCode != 200) {
throw Exception('Opera chat ${res.statusCode}');
}
final raw = utf8.decode(await res.stream.toBytes().timeout(requestTimeout));
final buf = StringBuffer();
for (final piece in extractAllSseContent(raw)) {
buf.write(piece);
}
if (buf.isEmpty) {
// v2 envelope: {"response":{"message":"..."}}
for (final line in raw.split('\n')) {
if (!line.contains('{')) continue;
try {
final data = jsonDecode(line.startsWith('data:') ? line.substring(5) : line);
if (data is Map) {
final msg = data['response'] is Map
? (data['response'] as Map)['message']
: data['message'];
if (msg is String) buf.write(msg);
}
} catch (_) {}
}
}
final text = buf.toString().trim();
if (!looksLikeModelOutput(text)) throw Exception('Opera empty');
yield text;
}
}