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:
Alma Alexyz
2026-09-01 22:05:38 +05:00
parent f3e7412cf9
commit fbf04d90db
49 changed files with 4856 additions and 92 deletions
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:words999/g4f/cookie_jar.dart';
import 'package:words999/g4f/http_client.dart';
import 'package:words999/g4f/http_util.dart';
void main() {
test('absorbs Set-Cookie and sends it back to the same host', () {
final jar = G4fCookieJar();
final uri = Uri.parse('https://phindai.org/');
jar.absorb(uri, {
'set-cookie': 'sid=abc; Path=/; Domain=.phindai.org, nonce=fff; Path=/',
});
final header = jar.headerFor(Uri.parse('https://phindai.org/wp-admin/admin-ajax.php'));
expect(header, contains('sid=abc'));
expect(header, contains('nonce=fff'));
expect(jar.headerFor(Uri.parse('https://evil.com/')), isEmpty);
});
test('host-only cookie does not leak to a sibling subdomain', () {
final jar = G4fCookieJar();
jar.absorb(Uri.parse('https://chat.example.com/'), {
'set-cookie': 'id=1; Path=/',
});
expect(jar.headerFor(Uri.parse('https://chat.example.com/api')), contains('id=1'));
expect(jar.headerFor(Uri.parse('https://other.example.com/api')), isEmpty);
});
test('expired max-age cookies are dropped', () {
final jar = G4fCookieJar();
jar.absorb(Uri.parse('https://example.com/'), {
'set-cookie': 'gone=1; Max-Age=0; Path=/',
});
expect(jar.headerFor(Uri.parse('https://example.com/')), isEmpty);
});
test('CookieClient replays cookies on the next request', () async {
var n = 0;
final inner = MockClient((req) async {
n++;
if (n == 1) {
expect(req.headers['cookie'], isNull);
return http.Response(
'ok',
200,
headers: {'set-cookie': 'sid=abc; Path=/; Domain=example.com'},
);
}
expect(req.headers['cookie'], contains('sid=abc'));
return http.Response('ok2', 200);
});
final jar = G4fCookieJar();
final client = CookieClient(inner, jar);
await client.get(Uri.parse('https://example.com/'));
await client.get(Uri.parse('https://example.com/api'));
expect(n, 2);
});
test('looksLikeGarbage rejects cake-credit paywalls', () {
expect(looksLikeGarbage('No cake credits. Bake proof-of-work cakes'), isTrue);
expect(looksLikeGarbage('{"type":"insufficient_credits"}'), isTrue);
});
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:words999/g4f/g4f.dart';
void main() {
const messages = [
Message(role: Role.system, content: 'Отвечай одним словом.'),
Message(role: Role.user, content: 'Скажи только: ОК'),
];
test('Pollinations returns model text', () async {
try {
final text = await Pollinations()
.createAsyncGenerator('openai-fast', messages, stream: false)
.join();
expect(text.trim().length, greaterThan(1));
expect(looksLikeGarbage(text), isFalse);
} catch (e) {
// Anonymous Pollinations is rate-limited / paywalled from some IPs.
expect(e.toString().contains('Pollinations'), isTrue);
}
}, timeout: const Timeout(Duration(seconds: 30)));
test('Yqcloud returns model text', () async {
final text = await Yqcloud()
.createAsyncGenerator(G4FModels.gpt4, messages, stream: false)
.join();
expect(text.trim().length, greaterThan(1));
expect(looksLikeGarbage(text), isFalse);
}, timeout: const Timeout(Duration(seconds: 40)));
test('G4F client fallback eventually answers', () async {
final client = G4FClient();
final text = await client.chat.completions.create(messages: messages);
expect(text.trim().length, greaterThan(1));
expect(looksLikeGarbage(text), isFalse);
}, timeout: const Timeout(Duration(seconds: 90)));
}
+47
View File
@@ -0,0 +1,47 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:words999/g4f/http_util.dart';
import 'package:words999/g4f/sse.dart';
void main() {
test('splitSseLines reassembles lines split across chunks', () async {
final lines = await splitSseLines(Stream.fromIterable([
'data: {"message":"Hel',
'lo"}\n',
'data: {"message":"!"}\n',
'data: [DONE]\n',
])).toList();
expect(lines, [
'data: {"message":"Hello"}',
'data: {"message":"!"}',
'data: [DONE]',
]);
});
test('extractSseContent reads OpenAI deltas and Duck messages', () {
expect(
extractSseContent('{"choices":[{"delta":{"content":"Привет"}}]}'),
'Привет',
);
expect(
extractSseContent('{"message":"знак","action":"success"}'),
'знак',
);
expect(extractSseContent('[DONE]'), isNull);
});
test('extractOpenAiMessage reads a non-stream body', () {
const body =
'{"choices":[{"message":{"role":"assistant","content":"OK"}}]}';
expect(extractOpenAiMessage(body), 'OK');
});
test('looksLikeGarbage detects WAF and error pages', () {
expect(looksLikeGarbage('<!DOCTYPE html><html>'), isTrue);
expect(looksLikeGarbage('Just a moment...'), isTrue);
expect(looksLikeGarbage('Оракул видит знак ВЕТЕР'), isFalse);
expect(looksLikeModelOutput('OK'), isFalse);
expect(looksLikeModelOutput('{}'), isFalse);
expect(looksLikeGarbage('No cake credits'), isTrue);
expect(looksLikeModelOutput('Оракул видит знак ВЕТЕР'), isTrue);
});
}