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 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 createAsyncGenerator( String model, List 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; } }