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.
138 lines
4.7 KiB
Dart
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;
|
|
}
|
|
}
|