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
+4
View File
@@ -43,4 +43,8 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android/build/
*.apk
# Local bakeoff dumps
/generated_media/
+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
android {
namespace = "com.example.words999.words999"
compileSdk = flutter.compileSdkVersion
compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
+14
View File
@@ -19,6 +19,20 @@ subprojects {
project.evaluationDependsOn(":app")
}
gradle.projectsEvaluated {
rootProject.subprojects.forEach { proj ->
val android = proj.extensions.findByName("android") ?: return@forEach
try {
android.javaClass.getMethod("setCompileSdk", Int::class.javaPrimitiveType).invoke(android, 36)
} catch (_: Throwable) {
try {
android.javaClass.getMethod("setCompileSdkVersion", Int::class.javaPrimitiveType).invoke(android, 36)
} catch (_: Throwable) {
}
}
}
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+83
View File
@@ -0,0 +1,83 @@
import 'models.dart';
import 'providers/aggregators.dart';
import 'providers/aichatos.dart';
import 'providers/base_provider.dart';
import 'providers/blackbox.dart';
import 'providers/chatgpt_oss.dart';
import 'providers/cloudflare.dart';
import 'providers/cohere_space.dart';
import 'providers/copilot_app.dart';
import 'providers/deep_ai.dart';
import 'providers/duckduckgo.dart';
import 'providers/free2gpt.dart';
import 'providers/glm.dart';
import 'providers/hugging_chat.dart';
import 'providers/liaobots.dart';
import 'providers/opera_aria.dart';
import 'providers/phind_ai.dart';
import 'providers/pollinations.dart';
import 'providers/qwen.dart';
import 'providers/quillbot.dart';
import 'providers/teach_anything.dart';
import 'providers/webview/deepinfra_webview.dart';
import 'providers/you_chat.dart';
import 'providers/yqcloud.dart';
import 'retry.dart';
class G4FChatCompletions {
/// REST-live first, then Python-hosted OpenAI-compat, then WAF/WebView.
final List<BaseProvider> _providers = [
Yqcloud(),
DeepAi(),
Quillbot(),
KiloCode(),
OpenCode(),
G4FSpace(),
Pollinations(),
PhindAi(),
TeachAnything(),
CopilotApp(),
ChatGptOss(),
AiChatOs(),
OperaAria(),
Liaobots(),
YouChat(),
Free2Gpt(),
CohereSpace(),
GlmZai(),
DuckDuckGo(),
Blackbox(),
HuggingChat(),
QwenChat(),
CloudflareAi(),
DeepInfraWebView(),
];
List<BaseProvider> get providers => List.unmodifiable(_providers);
late final IterListProvider _retry = IterListProvider(_providers);
/// Full completion via g4f IterListProvider (try next on failure).
Future<String> create({
required List<Message> messages,
String? model,
}) {
return _retry.create(messages: messages, model: model ?? G4FModels.gpt4o);
}
Stream<String> createStream({
required List<Message> messages,
String? model,
}) async* {
yield await create(messages: messages, model: model);
}
}
class G4FChat {
final completions = G4FChatCompletions();
}
/// Mirrors `g4f.client.Client`.
class G4FClient {
final chat = G4FChat();
}
+191
View File
@@ -0,0 +1,191 @@
/// In-process cookie jar. Python g4f keeps a curl_cffi session; Dart/Cronet
/// does not, so every provider must share cookies across GET-then-POST.
class G4fCookieJar {
final Map<String, _StoredCookie> _cookies = {};
int get length => _cookies.length;
void clear() => _cookies.clear();
void set({
required String name,
required String value,
required String domain,
String path = '/',
DateTime? expires,
bool secure = false,
bool hostOnly = true,
}) {
if (name.isEmpty) return;
final canon = _canonDomain(domain);
if (canon.isEmpty) return;
final key = _key(canon, path, name);
if (value.isEmpty && expires != null) {
_cookies.remove(key);
return;
}
_cookies[key] = _StoredCookie(
name: name,
value: value,
domain: canon,
path: path.isEmpty ? '/' : path,
expires: expires,
secure: secure,
hostOnly: hostOnly,
);
}
/// Parse `Set-Cookie` from an HTTP response for [uri].
void absorb(Uri uri, Map<String, String> headers) {
final raw = headers['set-cookie'];
if (raw == null || raw.isEmpty) return;
for (final part in splitSetCookieHeader(raw)) {
final parsed = _parseSetCookie(part, uri);
if (parsed == null) continue;
if (parsed.expired) {
_cookies.remove(_key(parsed.domain, parsed.path, parsed.name));
continue;
}
_cookies[_key(parsed.domain, parsed.path, parsed.name)] = parsed;
}
}
String headerFor(Uri uri) {
final now = DateTime.now().toUtc();
final matching = <_StoredCookie>[];
final stale = <String>[];
_cookies.forEach((key, cookie) {
if (cookie.expires != null && !cookie.expires!.isAfter(now)) {
stale.add(key);
return;
}
if (cookie.matches(uri)) matching.add(cookie);
});
for (final key in stale) {
_cookies.remove(key);
}
matching.sort((a, b) => b.path.length.compareTo(a.path.length));
return matching.map((c) => '${c.name}=${c.value}').join('; ');
}
/// Test helper.
Map<String, String> asMapFor(Uri uri) {
final header = headerFor(uri);
if (header.isEmpty) return {};
final out = <String, String>{};
for (final part in header.split('; ')) {
final eq = part.indexOf('=');
if (eq <= 0) continue;
out[part.substring(0, eq)] = part.substring(eq + 1);
}
return out;
}
}
class _StoredCookie {
_StoredCookie({
required this.name,
required this.value,
required this.domain,
required this.path,
required this.secure,
required this.hostOnly,
this.expires,
});
final String name;
final String value;
final String domain;
final String path;
final DateTime? expires;
final bool secure;
final bool hostOnly;
bool get expired {
if (expires == null) return false;
return !expires!.isAfter(DateTime.now().toUtc());
}
bool matches(Uri uri) {
if (secure && uri.scheme != 'https') return false;
final host = uri.host.toLowerCase();
if (hostOnly) {
if (host != domain) return false;
} else {
if (host != domain && !host.endsWith('.$domain')) return false;
}
final reqPath = uri.path.isEmpty ? '/' : uri.path;
if (path == '/') return true;
if (reqPath == path) return true;
if (reqPath.startsWith(path.endsWith('/') ? path : '$path/')) return true;
return false;
}
}
String _canonDomain(String domain) {
var d = domain.trim().toLowerCase();
if (d.startsWith('.')) d = d.substring(1);
return d;
}
String _key(String domain, String path, String name) =>
'${_canonDomain(domain)}|$path|$name';
/// Split a folded Set-Cookie header (commas inside Expires dates).
List<String> splitSetCookieHeader(String header) {
return header
.split(RegExp(r',(?=\s*[^;=]+=)'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
}
_StoredCookie? _parseSetCookie(String raw, Uri uri) {
final parts = raw.split(';');
if (parts.isEmpty) return null;
final nv = parts.first.trim();
final eq = nv.indexOf('=');
if (eq <= 0) return null;
final name = nv.substring(0, eq).trim();
final value = nv.substring(eq + 1).trim();
if (name.isEmpty) return null;
String? domain;
var path = '/';
DateTime? expires;
var secure = false;
var hostOnly = true;
for (var i = 1; i < parts.length; i++) {
final attr = parts[i].trim();
if (attr.isEmpty) continue;
final aEq = attr.indexOf('=');
final key = (aEq < 0 ? attr : attr.substring(0, aEq)).trim().toLowerCase();
final val = aEq < 0 ? '' : attr.substring(aEq + 1).trim();
if (key == 'domain' && val.isNotEmpty) {
domain = _canonDomain(val);
hostOnly = false;
} else if (key == 'path' && val.isNotEmpty) {
path = val;
} else if (key == 'expires') {
expires ??= DateTime.tryParse(val)?.toUtc();
} else if (key == 'max-age') {
final secs = int.tryParse(val);
if (secs != null) {
expires = DateTime.now().toUtc().add(Duration(seconds: secs));
}
} else if (key == 'secure') {
secure = true;
}
}
return _StoredCookie(
name: name,
value: value,
domain: domain ?? uri.host.toLowerCase(),
path: path,
expires: expires,
secure: secure,
hostOnly: domain == null ? true : hostOnly,
);
}
+33
View File
@@ -0,0 +1,33 @@
library g4f;
export 'models.dart';
export 'client.dart';
export 'http_util.dart';
export 'http_client.dart';
export 'retry.dart';
export 'sse.dart';
export 'providers/base_provider.dart';
export 'providers/chatgpt_oss.dart';
export 'providers/teach_anything.dart';
export 'providers/webview_base_provider.dart';
export 'providers/webview/deepinfra_webview.dart';
export 'providers/pollinations.dart';
export 'providers/yqcloud.dart';
export 'providers/deep_ai.dart';
export 'providers/phind_ai.dart';
export 'providers/duckduckgo.dart';
export 'providers/copilot_app.dart';
export 'providers/blackbox.dart';
export 'providers/hugging_chat.dart';
export 'providers/glm.dart';
export 'providers/cloudflare.dart';
export 'providers/qwen.dart';
export 'providers/openai_compat.dart';
export 'providers/quillbot.dart';
export 'providers/opera_aria.dart';
export 'providers/liaobots.dart';
export 'providers/you_chat.dart';
export 'providers/cohere_space.dart';
export 'cookie_jar.dart';
export 'providers/aggregators.dart';
export 'providers/free2gpt.dart';
+83
View File
@@ -0,0 +1,83 @@
import 'dart:io';
import 'package:cronet_http/cronet_http.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'cookie_jar.dart';
import 'http_util.dart';
/// Shared HTTP stack for every G4F provider.
///
/// Python g4f uses `curl_cffi(impersonate="chrome")` plus a session cookie
/// jar. On Android the closest TLS equivalent is Cronet; cookies are kept
/// in [g4fCookies] because Cronet does not expose a Dart cookie store.
http.Client createG4fClient() {
if (!kIsWeb && Platform.isAndroid) {
final engine = CronetEngine.build(
cacheMode: CacheMode.memory,
cacheMaxSize: 4 * 1024 * 1024,
userAgent: kDesktopUserAgent,
enableHttp2: true,
enableBrotli: true,
enableQuic: true,
);
return CronetClient.fromCronetEngine(engine);
}
return http.Client();
}
final G4fCookieJar g4fCookies = G4fCookieJar();
final http.Client g4fHttp = CookieClient(createG4fClient(), g4fCookies);
class CookieClient extends http.BaseClient {
CookieClient(this._inner, this.jar);
final http.Client _inner;
final G4fCookieJar jar;
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final fromJar = jar.headerFor(request.url);
if (fromJar.isNotEmpty) {
final existing = request.headers['cookie'];
if (existing == null || existing.isEmpty) {
request.headers['cookie'] = fromJar;
} else {
request.headers['cookie'] = '$existing; $fromJar';
}
}
final response = await _inner.send(request);
jar.absorb(request.url, response.headers);
return response;
}
@override
void close() => _inner.close();
}
final Set<String> _warmedOrigins = {};
/// GET the provider origin once so Cloudflare/session cookies land in [g4fCookies].
Future<void> warmupOrigin(String url) async {
if (url.isEmpty) return;
final uri = Uri.tryParse(url);
if (uri == null || !uri.hasScheme || uri.host.isEmpty) return;
final origin = uri.origin;
if (_warmedOrigins.contains(origin)) return;
_warmedOrigins.add(origin);
try {
await g4fHttp
.get(
uri,
headers: browserHeaders(
desktop: true,
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
referer: '$origin/',
),
)
.timeout(const Duration(seconds: 12));
} catch (_) {}
}
+75
View File
@@ -0,0 +1,75 @@
const kBrowserUserAgent =
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36';
const kDesktopUserAgent =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
Map<String, String> browserHeaders({
String? origin,
String? referer,
String? accept,
String? contentType,
bool desktop = false,
Map<String, String>? extra,
}) {
return {
'User-Agent': desktop ? kDesktopUserAgent : kBrowserUserAgent,
'Accept': accept ?? '*/*',
'Accept-Language': 'en-US,en;q=0.9',
if (origin != null) 'Origin': origin,
if (referer != null) 'Referer': referer,
if (contentType != null) 'Content-Type': contentType,
...?extra,
};
}
/// True when a provider dumped an error page / WAF / paywall instead of model text.
bool looksLikeGarbage(String text) {
final t = text.trimLeft();
if (t.isEmpty) return true;
final lower = t.toLowerCase();
if (t.startsWith('<!DOCTYPE') || t.startsWith('<html') || t.startsWith('<HTML')) {
return true;
}
if (lower.contains('just a moment') ||
lower.contains('cf-browser-verification') ||
lower.contains('attention required') ||
lower.contains('payment required') ||
lower.contains('frontend_captcha') ||
lower.contains('err_challenge') ||
lower.contains('error code:') ||
lower.contains('missing api key') ||
lower.contains('invalid api key') ||
lower.contains('you have to be logged in') ||
lower.contains('no cake credits') ||
lower.contains('insufficient_credits')) {
return true;
}
return false;
}
bool looksLikeModelOutput(String text) {
final trimmed = text.trim();
if (trimmed.length < 8) return false;
final lower = trimmed.toLowerCase();
if (lower == ': ping' || lower == 'ping' || lower == 'pong' || lower == '{}') {
return false;
}
if (RegExp(r'^[\{\}\[\]":\s]+$').hasMatch(trimmed)) return false;
return !looksLikeGarbage(trimmed);
}
String? cookieHeaderFromSetCookie(String? setCookie, {String? name}) {
if (setCookie == null || setCookie.isEmpty) return null;
// http package may join multiple Set-Cookie values with ", ".
final parts = setCookie.split(RegExp(r',(?=\s*[^;=]+=)'));
final pairs = <String>[];
for (final part in parts) {
final nv = part.split(';').first.trim();
if (nv.isEmpty || !nv.contains('=')) continue;
if (name != null && !nv.startsWith('$name=')) continue;
pairs.add(nv);
}
if (pairs.isEmpty) return null;
return pairs.join('; ');
}
+26
View File
@@ -0,0 +1,26 @@
// models.dart
enum Role { system, user, assistant }
class Message {
final Role role;
final String content;
const Message({required this.role, required this.content});
Map<String, dynamic> toJson() => {
'role': role.name,
'content': content,
};
}
class G4FModels {
static const String gpt4 = 'gpt-4';
static const String gpt4o = 'gpt-4o';
static const String gpt4oMini = 'gpt-4o-mini';
static const String commandR = 'command-r';
static const String commandRPlus = 'command-r-plus';
static const String mistral = 'mistral';
static const String llama3 = 'llama-3';
static const String qwen = 'qwen';
static const String openaiFast = 'openai-fast';
}
+90
View File
@@ -0,0 +1,90 @@
import '../http_util.dart';
import '../models.dart';
import 'base_provider.dart';
import 'openai_compat.dart';
/// Python `KiloCode` — OpenAI-compat gateway, no key for `kilo-auto/free`.
class KiloCode extends OpenAiCompatProvider {
KiloCode()
: super(
providerName: 'KiloCode',
apiBase: 'https://api.kilo.ai/api/gateway',
fallbackModel: 'kilo-auto/free',
supportedModels: const ['kilo-auto/free', 'kilo-auto'],
);
@override
String get url => 'https://kilo.ai';
}
/// Python `OpenCode` — `https://opencode.ai/zen/v1`, default `big-pickle`.
class OpenCode extends OpenAiCompatProvider {
OpenCode()
: super(
providerName: 'OpenCode',
apiBase: 'https://opencode.ai/zen/v1',
fallbackModel: 'big-pickle',
supportedModels: const ['big-pickle'],
);
@override
String get url => 'https://opencode.ai';
}
/// Python `G4FSpace` / `ClientFactory("default")`.
/// Anonymous calls currently return 402 cake-credits; still tried so a baked
/// session cookie or future free window works without a code change.
class G4FSpace extends BaseProvider {
@override
String get name => 'G4FSpace';
@override
String get url => 'https://g4f.space';
@override
bool get working => true;
@override
List<String> get models => const ['auto', 'gpt-4o', 'openai'];
@override
String get defaultModel => 'auto';
static const _bases = [
'https://g4f.space/api/auto',
'https://g4f.space/api/pollinations',
'https://g4f.space/v1',
];
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final errors = <String>[];
for (final base in _bases) {
try {
final inner = OpenAiCompatProvider(
providerName: 'G4FSpace',
apiBase: base,
fallbackModel: model.isEmpty ? defaultModel : model,
supportedModels: models,
);
final text = await inner
.complete(model.isEmpty ? defaultModel : model, messages)
.timeout(requestTimeout);
if (looksLikeModelOutput(text)) {
yield text;
return;
}
errors.add('$base unusable');
} catch (e) {
errors.add('$base: $e');
}
}
throw Exception('G4FSpace failed: ${errors.join(' | ')}');
}
}
+63
View File
@@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import '../models.dart';
import 'base_provider.dart';
class AiChatOs extends BaseProvider {
@override
String get name => 'AiChatOs';
@override
bool get working => true;
@override
List<String> get models => [G4FModels.gpt4];
@override
String get defaultModel => G4FModels.gpt4;
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final url = Uri.parse('https://api.aichatos.cloud/api/generateStream');
final userId = '#/chat/${DateTime.now().millisecondsSinceEpoch}';
final systemMessage = extractSystemMessage(messages);
final formattedPrompt = formatPrompt(messages);
final req = http.Request('POST', url);
req.headers.addAll({
'Content-Type': 'application/json',
'Origin': 'https://chat.aichatos.top',
'Referer': 'https://chat.aichatos.top/',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
});
req.body = jsonEncode({
'prompt': formattedPrompt,
'userId': userId,
'network': true,
'system': systemMessage,
'withoutContext': false,
'stream': stream,
});
final response = await g4fHttp.send(req);
if (response.statusCode == 200) {
final resBytes = await response.stream.toBytes();
final fullText = utf8.decode(resBytes).trim();
if (fullText.isNotEmpty && !fullText.startsWith('<!DOCTYPE') && !fullText.contains('Error:')) {
yield fullText;
} else {
throw Exception('$name unusable body');
}
} else {
throw Exception('$name returned status ${response.statusCode}');
}
}
}
+103
View File
@@ -0,0 +1,103 @@
import '../models.dart';
/// Base abstract class for all G4F providers.
/// Mirrors `AsyncGeneratorProvider` in xtekky/gpt4free.
abstract class BaseProvider {
/// User-friendly name of the provider
String get name;
String get label => name;
String get url => '';
/// Whether the provider is currently believed to be working
bool get working;
/// List of supported models by this provider
List<String> get models;
/// Optional fallback default model if none specified
String get defaultModel;
/// Whether this provider needs a hidden WebView (slow, device-only).
bool get isWebView => false;
/// Per-provider deadline. Python g4f defaults to ~120s; we keep a minute.
Duration get requestTimeout => const Duration(seconds: 60);
/// g4f `model_aliases` (e.g. gpt-4o → provider default).
Map<String, String> get modelAliases => const {};
String resolveModel(String? model) {
if (model == null || model.isEmpty) return defaultModel;
if (models.contains(model)) return model;
final alias = modelAliases[model];
if (alias != null) return alias;
return defaultModel;
}
/// Whether the provider can emit incremental tokens.
bool get supportsStream => false;
/// Full reply. The Oracle never streams — it waits for a complete text.
Future<String> complete(String model, List<Message> messages) async {
final buf = StringBuffer();
await for (final chunk in createAsyncGenerator(
model,
messages,
stream: false,
).timeout(requestTimeout)) {
buf.write(chunk);
}
return buf.toString();
}
/// Convert chat history to OpenAI-style JSON maps.
List<Map<String, String>> toOpenAiMessages(List<Message> messages) {
return [
for (final m in messages) {'role': m.role.name, 'content': m.content},
];
}
/// Helper to get last user message
String getLastUserMessage(List<Message> messages) {
for (var i = messages.length - 1; i >= 0; i--) {
if (messages[i].role == Role.user) {
return messages[i].content;
}
}
return '';
}
/// Helper to extract system prompt if supported by the provider
String extractSystemMessage(List<Message> messages) {
final sys = messages.where((m) => m.role == Role.system);
if (sys.isNotEmpty) {
return sys.map((m) => m.content).join('\n');
}
return '';
}
/// Helper to format a full prompt string (for providers without native chat context)
String formatPrompt(List<Message> messages, {bool includeSystem = true}) {
final sb = StringBuffer();
for (final m in messages) {
if (m.role == Role.system) {
if (includeSystem) sb.writeln('System: ${m.content}');
} else if (m.role == Role.user) {
sb.writeln('User: ${m.content}');
} else {
sb.writeln('Assistant: ${m.content}');
}
}
return sb.toString();
}
/// Main interface to execute a chat completion synchronously/asynchronously.
/// Streams chunked output if `stream` is supported.
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
});
}
+213
View File
@@ -0,0 +1,213 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import 'package:uuid/uuid.dart';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'webview_base_provider.dart';
/// Blackbox web chat. Tries REST after scraping a `validated` token, then WebView.
class Blackbox extends WebViewBaseProvider {
@override
String get name => 'Blackbox';
@override
bool get working => true;
@override
List<String> get models => ['blackboxai', 'gpt-4o'];
@override
String get defaultModel => 'blackboxai';
@override
String get baseUrl => 'https://www.blackbox.ai/';
@override
String get apiEndpoint => 'https://www.blackbox.ai/api/chat';
static const _uuid = Uuid();
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
try {
await for (final chunk in _tryRest(messages, stream: stream)) {
yield chunk;
}
return;
} catch (_) {}
await for (final chunk in super.createAsyncGenerator(model, messages, stream: stream)) {
yield chunk;
}
}
Stream<String> _tryRest(List<Message> messages, {required bool stream}) async* {
final validated = await _scrapeValidated();
final payload = {
'messages': [
for (final m in messages)
{
'id': _uuid.v4(),
'content': m.content,
'role': m.role.name,
}
],
'id': _uuid.v4(),
'previewToken': null,
'userId': null,
'codeModelMode': true,
'agentMode': {},
'trendingAgentMode': {},
'isMicMode': false,
'maxTokens': 1024,
'isChromeExt': false,
'clickedAnswer2': false,
'clickedAnswer3': false,
'clickedForceWebSearch': false,
'visitFromDelta': false,
'mobileClient': false,
'userSelectedModel': null,
if (validated != null) 'validated': validated,
'imageGenerationMode': false,
'webSearchMode': false,
};
final urls = [
'https://www.blackbox.ai/api/chat',
'https://api.blackbox.ai/api/chat',
];
Object? lastError;
for (final url in urls) {
try {
final req = http.Request('POST', Uri.parse(url));
req.headers.addAll(browserHeaders(
origin: 'https://www.blackbox.ai',
referer: 'https://www.blackbox.ai/',
contentType: 'application/json',
));
req.body = jsonEncode(payload);
final res = await g4fHttp.send(req).timeout(const Duration(seconds: 20));
if (res.statusCode != 200) {
lastError = 'HTTP ${res.statusCode}';
continue;
}
final collected = StringBuffer();
await for (final chunk in res.stream.transform(utf8.decoder)) {
if (chunk.isEmpty) continue;
// Blackbox sometimes streams raw text, sometimes SSE.
if (chunk.trimLeft().startsWith('data:')) {
for (final piece in extractAllSseContent(chunk)) {
collected.write(piece);
if (stream) yield piece;
}
} else {
collected.write(chunk);
if (stream) yield chunk;
}
}
final text = collected.toString().trim();
if (!looksLikeModelOutput(text)) {
lastError = 'unusable body';
continue;
}
if (!stream) yield text;
return;
} catch (e) {
lastError = e;
}
}
throw Exception('Blackbox REST failed: $lastError');
}
Future<String?> _scrapeValidated() async {
try {
final page = await g4fHttp.get(
Uri.parse('https://www.blackbox.ai/'),
headers: browserHeaders(referer: 'https://www.blackbox.ai/'),
).timeout(const Duration(seconds: 12));
if (page.statusCode != 200) return null;
final chunks = RegExp(r'/_next/static/[^"\x27]+\.js').allMatches(page.body);
final uuidRe = RegExp(
r'[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}',
caseSensitive: false,
);
for (final m in chunks.take(20)) {
final src = m.group(0)!;
final js = await g4fHttp.get(
Uri.parse('https://www.blackbox.ai$src'),
headers: browserHeaders(),
).timeout(const Duration(seconds: 8));
if (js.statusCode != 200) continue;
if (!js.body.contains('validated') && !js.body.contains('api/chat')) {
continue;
}
final match = uuidRe.firstMatch(js.body);
if (match != null) return match.group(0);
}
} catch (_) {}
return null;
}
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final messagesJson = jsonEncode([
for (final m in messages)
{'id': _uuid.v4(), 'content': m.content, 'role': m.role.name}
]);
return '''
(async function() {
const payload = {
messages: $messagesJson,
id: crypto.randomUUID(),
previewToken: null,
userId: null,
codeModelMode: true,
agentMode: {},
trendingAgentMode: {},
isMicMode: false,
maxTokens: 1024
};
const urls = ['/api/chat', 'https://www.blackbox.ai/api/chat'];
for (const url of urls) {
try {
const resp = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (resp.status === 200) {
return await resp.text();
}
} catch (e) {}
}
return JSON.stringify({error: 'Blackbox chat endpoint not available'});
})();
''';
}
@override
String parseResponse(String rawResponse) {
if (looksLikeModelOutput(rawResponse) && !rawResponse.trim().startsWith('{')) {
return rawResponse.trim();
}
try {
final json = jsonDecode(rawResponse);
if (json is Map && json['error'] != null) {
throw Exception(json['error']);
}
} catch (e) {
if (e.toString().contains('Blackbox') || e.toString().contains('error')) {
rethrow;
}
}
final sse = extractAllSseContent(rawResponse).join();
if (looksLikeModelOutput(sse)) return sse;
if (looksLikeModelOutput(rawResponse)) return rawResponse.trim();
throw Exception('Failed to parse Blackbox: ${rawResponse.substring(0, rawResponse.length.clamp(0, 180))}');
}
}
+98
View File
@@ -0,0 +1,98 @@
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import '../models.dart';
import '../sse.dart';
import 'base_provider.dart';
class ChatGptOss extends BaseProvider {
@override
String get name => 'ChatGptOss';
@override
bool get working => true;
@override
List<String> get models => ['gpt-oss-120b', 'gpt-5-nano'];
@override
String get defaultModel => 'gpt-oss-120b';
String _generateFingerprint() {
final seed = '${DateTime.now().millisecondsSinceEpoch / 1000.0}-${Random().nextDouble()}';
return md5.convert(utf8.encode(seed)).toString();
}
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final prompt = formatPrompt(messages);
final url = Uri.parse('https://chat-gpt-oss.com/api/message');
final payload = {
"conversation_id": null,
"model": model,
"content": prompt,
"reasoning_effort": "low",
};
final req = http.Request('POST', url);
req.headers.addAll({
"accept": "text/event-stream",
"accept-language": "en-US,en;q=0.9",
"cache-control": "no-cache",
"content-type": "application/json",
"origin": "https://chat-gpt-oss.com",
"pragma": "no-cache",
"referer": "https://chat-gpt-oss.com/",
"sec-ch-ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Linux"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
"x-fingerprint": _generateFingerprint(),
});
req.body = jsonEncode(payload);
final response = await g4fHttp.send(req);
if (response.statusCode == 200) {
String? currentEvent;
await for (final line in splitSseLines(response.stream.transform(utf8.decoder))) {
final trimmed = line.trim();
if (trimmed.isEmpty) {
currentEvent = null;
continue;
}
if (trimmed.startsWith('event:')) {
currentEvent = trimmed.substring(6).trim();
if (currentEvent == 'summary') {
return;
}
continue;
}
if (trimmed.startsWith('data:') && currentEvent == 'message') {
final dataStr = trimmed.substring(5).trim();
if (dataStr.isEmpty) continue;
try {
final jsonChunk = jsonDecode(dataStr);
final content = jsonChunk['content'] as String?;
if (content != null && content.isNotEmpty) {
yield content;
}
} catch (_) {}
}
}
} else {
throw Exception('$name returned status ${response.statusCode}');
}
}
}
+110
View File
@@ -0,0 +1,110 @@
import 'dart:convert';
import '../http_util.dart';
import '../models.dart';
import 'webview_base_provider.dart';
/// Cloudflare Workers AI playground via an in-page WebSocket.
class CloudflareAi extends WebViewBaseProvider {
@override
String get name => 'CloudflareAI';
@override
bool get working => true;
@override
List<String> get models => [
'@cf/meta/llama-3.3-70b-instruct-fp8-fast',
'@cf/meta/llama-3.1-8b-instruct-fp8',
'@cf/google/gemma-3-12b-it',
];
@override
String get defaultModel => '@cf/meta/llama-3.3-70b-instruct-fp8-fast';
@override
String get baseUrl => 'https://playground.ai.cloudflare.com';
@override
String get apiEndpoint => 'wss://playground.ai.cloudflare.com/agents/playground';
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final cfMessages = [
for (var i = 0; i < messages.length; i++)
{
'role': messages[i].role.name,
'parts': [
{'type': 'text', 'text': messages[i].content}
],
'id': 'msg_$i',
}
];
final messagesJson = jsonEncode(cfMessages);
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
return '''
(async function() {
const modelStr = $modelJson;
const pk = crypto.randomUUID();
const agentId = 'playground-' + Math.random().toString(36).slice(2, 15);
const wsUrl = 'wss://playground.ai.cloudflare.com/agents/playground/' + agentId
+ '?_pk=' + pk + '&model=' + encodeURIComponent(modelStr);
return await new Promise((resolve, reject) => {
const ws = new WebSocket(wsUrl);
let text = '';
const timer = setTimeout(() => {
try { ws.close(); } catch (e) {}
if (text) resolve(text); else reject('Cloudflare timeout');
}, 28000);
ws.onopen = () => {
ws.send(JSON.stringify({type:'cf_agent_stream_resume_request'}));
ws.send(JSON.stringify({name:agentId, agent:'playground', type:'cf_agent_identity'}));
ws.send(JSON.stringify({state:{model:modelStr, temperature:1, stream:true, system:'You are a helpful assistant.', useExternalProvider:false, externalProvider:'openai', generativeUI:false}, type:'cf_agent_state'}));
ws.send(JSON.stringify({mcp:{prompts:[], resources:[], servers:{}, tools:[]}, type:'cf_agent_mcp_servers'}));
ws.send(JSON.stringify({type:'cf_agent_stream_resume_none'}));
setTimeout(() => {
ws.send(JSON.stringify({
id: 'req_' + Math.random().toString(36).slice(2, 10),
init: { method: 'POST', body: JSON.stringify({ messages: $messagesJson, trigger: 'submit-message' }) },
type: 'cf_agent_use_chat_request'
}));
}, 800);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'cf_agent_use_chat_response') {
const body = JSON.parse(data.body);
if (body.type === 'text-delta' && body.delta) text += body.delta;
if (body.type === 'finish' || body.type === 'finish-step') {
clearTimeout(timer);
ws.close();
resolve(text);
}
}
} catch (e) {}
};
ws.onerror = () => { clearTimeout(timer); reject('Cloudflare WS error'); };
});
})();
''';
}
@override
String parseResponse(String rawResponse) {
final text = rawResponse.trim();
if (text.startsWith('{')) {
try {
final json = jsonDecode(text);
if (json is Map && json['error'] != null) {
throw Exception(json['error']);
}
} catch (e) {
if (e.toString().contains('Exception:')) rethrow;
}
}
if (!looksLikeModelOutput(text)) {
throw Exception('Cloudflare empty: ${text.substring(0, text.length.clamp(0, 160))}');
}
return text;
}
}
+98
View File
@@ -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;
}
}
+270
View File
@@ -0,0 +1,270 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:uuid/uuid.dart';
import '../http_client.dart';
import '../http_util.dart';
import '../models.dart';
import 'base_provider.dart';
import 'webview_base_provider.dart';
/// Microsoft Copilot: Cronet session + WebSocket, then origin WebView (nodriver analog).
class CopilotApp extends BaseProvider {
@override
String get name => 'CopilotApp';
@override
String get url => 'https://copilot.microsoft.com';
@override
bool get working => !kIsWeb;
@override
Duration get requestTimeout => const Duration(seconds: 90);
@override
List<String> get models => ['smart', 'chat', 'reasoning'];
@override
String get defaultModel => 'smart';
static const _ua =
'CopilotNative/30.0.440505001-prod (Android 14; Google; Pixel 8 Pro)';
static const _startUrl = 'https://copilot.microsoft.com/c/api/start';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
if (kIsWeb) {
throw UnsupportedError('CopilotApp requires dart:io (Android/iOS/desktop)');
}
final seeds = _fakeCookieSeeds();
_seedFakeCookies(seeds);
final prompt = formatPrompt(messages);
try {
yield await _viaRestAndWs(model, prompt);
return;
} catch (e) {
debugPrint('Copilot REST/WS failed, trying origin WebView: $e');
}
yield await _viaWebView(model, prompt, seeds);
}
Future<String> _viaRestAndWs(String model, String prompt) async {
final startReq = http.Request('POST', Uri.parse(_startUrl));
startReq.headers.addAll({
'content-type': 'application/json',
'user-agent': _ua,
'origin': url,
'referer': '$url/',
'X-Search-UILang': 'en-US',
});
startReq.body = jsonEncode({
'timeZone': 'Europe/Moscow',
'startNewConversation': true,
'teenSupportEnabled': true,
'correctPersonalizationSetting': true,
'deferredDataUseCapable': true,
});
final startRes = await g4fHttp.send(startReq).timeout(const Duration(seconds: 15));
if (startRes.statusCode != 200) {
final err = await startRes.stream.bytesToString();
throw Exception('Copilot start ${startRes.statusCode}: $err');
}
final startBody = jsonDecode(utf8.decode(await startRes.stream.toBytes()));
final conversationId = (startBody['currentConversationId'] ?? '').toString();
if (conversationId.isEmpty) {
throw Exception('Copilot: no conversation id');
}
final cookieHeader = g4fCookies.headerFor(Uri.parse(url));
final sessionId = const Uuid().v4();
final wsUrl =
'wss://copilot.microsoft.com/c/api/chat?api-version=2&clientSessionId=$sessionId';
final ws = await WebSocket.connect(
wsUrl,
headers: {
'User-Agent': _ua,
'Origin': url,
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,
},
).timeout(const Duration(seconds: 12));
try {
final mode = _modeFor(model);
var connected = false;
final collected = StringBuffer();
await for (final raw in ws.timeout(const Duration(seconds: 35))) {
if (raw is! String) continue;
Map<String, dynamic> data;
try {
data = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
continue;
}
final event = data['event']?.toString();
if (event == 'connected' || event == 'received') {
if (!connected) {
connected = true;
ws.add(jsonEncode({
'event': 'send',
'conversationId': conversationId,
'content': [
{'type': 'text', 'text': prompt},
],
'mode': mode,
}));
}
continue;
}
if (event == 'appendText' || event == 'replaceText') {
final text = data['text']?.toString() ?? '';
if (text.isNotEmpty) collected.write(text);
} else if (event == 'done') {
break;
} else if (event == 'error') {
throw Exception('Copilot error: $data');
}
}
final text = collected.toString().trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Copilot returned empty response');
}
return text;
} finally {
await ws.close();
}
}
Future<String> _viaWebView(
String model,
String prompt,
List<CookieSeed> seeds,
) async {
final promptJson = jsonEncode(prompt);
final modeJson = jsonEncode(_modeFor(model));
final raw = await runOriginWebView(
originUrl: url,
timeout: requestTimeout,
seeds: seeds,
functionBody: '''
const prompt = $promptJson;
const mode = $modeJson;
const start = await fetch('/c/api/start', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
startNewConversation: true,
teenSupportEnabled: true,
correctPersonalizationSetting: true,
deferredDataUseCapable: true
})
});
if (!start.ok) {
throw new Error('start ' + start.status + ' ' + (await start.text()).slice(0, 160));
}
const started = await start.json();
const conversationId = started.currentConversationId;
if (!conversationId) throw new Error('no conversation id');
const sessionId = (crypto.randomUUID && crypto.randomUUID()) || String(Math.random()).slice(2);
return await new Promise((resolve, reject) => {
const ws = new WebSocket('wss://copilot.microsoft.com/c/api/chat?api-version=2&clientSessionId=' + sessionId);
let text = '';
let sent = false;
const timer = setTimeout(() => { try { ws.close(); } catch (e) {} reject(new Error('ws timeout')); }, 45000);
ws.onmessage = (e) => {
let data;
try { data = JSON.parse(e.data); } catch (err) { return; }
const event = data.event;
if ((event === 'connected' || event === 'received') && !sent) {
sent = true;
ws.send(JSON.stringify({
event: 'send',
conversationId,
content: [{ type: 'text', text: prompt }],
mode
}));
}
if (event === 'appendText' || event === 'replaceText') text += data.text || '';
if (event === 'done') { clearTimeout(timer); ws.close(); resolve(text); }
if (event === 'error') { clearTimeout(timer); reject(new Error(JSON.stringify(data))); }
};
ws.onerror = () => { clearTimeout(timer); reject(new Error('ws error')); };
});
''',
);
final text = raw.trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Copilot WebView unusable: ${text.substring(0, text.length.clamp(0, 180))}');
}
return text;
}
void _seedFakeCookies(List<CookieSeed> seeds) {
final uri = Uri.parse(url);
for (final seed in seeds) {
g4fCookies.set(
name: seed.name,
value: seed.value,
domain: seed.domain ?? 'microsoft.com',
path: seed.path,
secure: seed.isSecure,
hostOnly: false,
);
}
// Touch the jar host as well as the parent domain.
g4fCookies.set(
name: '_C_ETH',
value: '1',
domain: uri.host,
secure: true,
);
}
List<CookieSeed> _fakeCookieSeeds() {
return [
CookieSeed(name: '_C_ETH', value: '1', domain: '.microsoft.com'),
CookieSeed(name: '_C_Auth', value: '', domain: '.microsoft.com'),
CookieSeed(name: 'MUID', value: _hex(32), domain: '.microsoft.com'),
CookieSeed(name: 'MUIDB', value: _hex(32), domain: '.microsoft.com'),
CookieSeed(
name: '_EDGE_S',
value: 'F=1&SID=${_hex(32)}',
domain: '.microsoft.com',
),
CookieSeed(name: '_EDGE_V', value: '1', domain: '.microsoft.com'),
];
}
String _modeFor(String model) {
final m = model.toLowerCase();
if (m.contains('reason') || m.contains('think') || m.contains('o1')) {
return 'reasoning';
}
if (m.contains('chat') || m.contains('gpt-4')) return 'chat';
return 'smart';
}
String _hex(int len) {
const hex = '0123456789ABCDEF';
final r = Random();
return String.fromCharCodes(
Iterable.generate(len, (_) => hex.codeUnitAt(r.nextInt(16))),
);
}
}
+118
View File
@@ -0,0 +1,118 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import 'package:crypto/crypto.dart';
import 'package:uuid/uuid.dart';
import '../models.dart';
import 'base_provider.dart';
class DeepAi extends BaseProvider {
@override
String get name => 'DeepAI';
@override
String get label => 'DeepAI';
@override
String get url => 'https://deepai.org/chat';
@override
bool get working => true;
@override
bool get supportsStream => true;
@override
String get defaultModel => 'standard';
@override
Map<String, String> get modelAliases => const {
G4FModels.gpt4: 'standard',
G4FModels.gpt4o: 'standard',
G4FModels.gpt4oMini: 'standard',
};
@override
List<String> get models => [
'standard',
'online',
'gemma-4',
'gemini-2.5-flash-lite',
'deepseek-v3.2',
];
String _reverseString(String input) {
return input.split('').reversed.join('');
}
String _myHashFunction(String inputStr) {
var bytes = utf8.encode(inputStr);
var digest = md5.convert(bytes);
return _reverseString(digest.toString());
}
String _generateApiKey(String userAgent) {
final random = Random();
final myRandomStr = (random.nextDouble() * 100000000000).round().toString();
final hash1 = _myHashFunction(
userAgent + myRandomStr + "hackers_become_a_little_stinkier_every_time_they_hack");
final hash2 = _myHashFunction(userAgent + hash1);
final hash3 = _myHashFunction(userAgent + hash2);
return "tryit-$myRandomStr-$hash3";
}
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = true,
}) async* {
final userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
final apiKey = _generateApiKey(userAgent);
final headers = {
'api-key': apiKey,
'user-agent': userAgent,
'origin': 'https://deepai.org',
'referer': 'https://deepai.org/chat',
};
final cleanedMessages = messages.map((m) => {'role': m.role.name, 'content': m.content}).toList();
var request = http.MultipartRequest('POST', Uri.parse('https://api.deepai.org/hacking_is_a_serious_crime'));
request.headers.addAll(headers);
request.fields['chat_style'] = 'chat';
request.fields['chatHistory'] = jsonEncode(cleanedMessages);
request.fields['model'] = model;
request.fields['session_uuid'] = const Uuid().v4();
request.fields['sensitivity_request_id'] = const Uuid().v4();
request.fields['hacker_is_stinky'] = 'very_stinky';
request.fields['enabled_tools'] = jsonEncode(["image_generator", "image_editor"]);
final response = await g4fHttp.send(request).timeout(requestTimeout);
if (response.statusCode != 200) {
final errorText = await response.stream.bytesToString();
throw Exception('DeepAI API returned status: ${response.statusCode} $errorText');
}
const fs = '\x1c';
var buffer = '';
await for (final chunk in response.stream.transform(utf8.decoder)) {
if (chunk.isEmpty) continue;
if (chunk.contains(fs) || buffer.contains(fs)) {
buffer += chunk;
final parts = buffer.split(fs);
if (parts[0].trim().isNotEmpty) {
yield parts[0];
}
break;
}
yield chunk;
}
}
}
+183
View File
@@ -0,0 +1,183 @@
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 'webview_base_provider.dart';
/// Duck.ai / DuckDuckGo chat. REST first; WebView executes the JS anti-bot hash.
class DuckDuckGo extends WebViewBaseProvider {
@override
String get name => 'DuckDuckGo';
@override
bool get working => true;
@override
List<String> get models => [
'gpt-5.6-luna',
'gpt-5.4-mini',
'claude-haiku-4-5',
'mistralai/Mistral-Small-24B-Instruct-2501',
'meta-llama/Llama-4-Scout',
];
@override
String get defaultModel => 'gpt-5.6-luna';
@override
String get baseUrl => 'https://duck.ai/';
@override
String get apiEndpoint => 'https://duckduckgo.com/duckchat/v1/chat';
static const _statusUrl = 'https://duckduckgo.com/duckchat/v1/status';
static const _chatUrl = 'https://duckduckgo.com/duckchat/v1/chat';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
try {
await for (final chunk in _tryRest(model, messages, stream: stream)) {
yield chunk;
}
return;
} catch (_) {
// 418 challenge — fall through to a real browser context.
}
await for (final chunk in super.createAsyncGenerator(model, messages, stream: stream)) {
yield chunk;
}
}
Stream<String> _tryRest(
String model,
List<Message> messages, {
required bool stream,
}) async* {
final statusReq = http.Request('GET', Uri.parse(_statusUrl));
statusReq.headers.addAll(browserHeaders(
origin: 'https://duckduckgo.com',
referer: 'https://duckduckgo.com/',
accept: 'application/json',
extra: {'x-vqd-accept': '1'},
));
final statusRes = await g4fHttp.send(statusReq).timeout(const Duration(seconds: 12));
final vqd4 = statusRes.headers['x-vqd-4'];
final hash1 = statusRes.headers['x-vqd-hash-1'];
await statusRes.stream.drain();
if (vqd4 == null || vqd4.isEmpty) {
throw Exception('DuckDuckGo: no x-vqd-4 (challenge required)');
}
final chatReq = http.Request('POST', Uri.parse(_chatUrl));
chatReq.headers.addAll(browserHeaders(
origin: 'https://duckduckgo.com',
referer: 'https://duckduckgo.com/',
contentType: 'application/json',
accept: 'text/event-stream',
extra: {
'x-vqd-4': vqd4,
if (hash1 != null && hash1.isNotEmpty) 'x-vqd-hash-1': hash1,
},
));
chatReq.body = jsonEncode({
'model': models.contains(model) ? model : defaultModel,
'messages': toOpenAiMessages(messages),
'canUseTools': false,
'canUseApproxLocation': false,
});
final chatRes = await g4fHttp.send(chatReq).timeout(const Duration(seconds: 25));
if (chatRes.statusCode != 200) {
throw Exception('DuckDuckGo chat ${chatRes.statusCode}');
}
final collected = StringBuffer();
await for (final line in splitSseLines(chatRes.stream.transform(utf8.decoder))) {
final trimmed = line.trim();
if (trimmed.isEmpty || !trimmed.startsWith('data:')) continue;
final content = extractSseContent(trimmed.substring(5).trim());
if (content != null && content.isNotEmpty) {
collected.write(content);
if (stream) yield content;
}
}
final text = collected.toString();
if (!looksLikeModelOutput(text)) {
throw Exception('DuckDuckGo empty/challenge response');
}
if (!stream) yield text;
}
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final messagesJson = jsonEncode(toOpenAiMessages(messages));
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
return '''
(async function() {
const status = await fetch('https://duckduckgo.com/duckchat/v1/status', {
headers: { 'x-vqd-accept': '1', 'Accept': 'application/json' }
});
let vqd4 = status.headers.get('x-vqd-4') || '';
let hash1 = status.headers.get('x-vqd-hash-1') || '';
if (hash1) {
try {
let src = hash1;
try { src = atob(hash1); } catch (e) {}
if (src.includes('function') || src.trim().startsWith('(')) {
const result = await (0, eval)(src);
const value = (typeof result === 'function') ? await result() : result;
hash1 = btoa(JSON.stringify(value));
}
} catch (e) {
return JSON.stringify({error: 'hash failed: ' + e});
}
}
const headers = {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
};
if (vqd4) headers['x-vqd-4'] = vqd4;
if (hash1) headers['x-vqd-hash-1'] = hash1;
const resp = await fetch('https://duckduckgo.com/duckchat/v1/chat', {
method: 'POST',
headers,
body: JSON.stringify({
model: $modelJson,
messages: $messagesJson,
canUseTools: false,
canUseApproxLocation: false
})
});
const body = await resp.text();
return JSON.stringify({status: resp.status, body: body});
})();
''';
}
@override
String parseResponse(String rawResponse) {
try {
final json = jsonDecode(rawResponse);
if (json is Map && json['error'] != null) {
throw Exception(json['error']);
}
final body = json is Map ? (json['body']?.toString() ?? rawResponse) : rawResponse;
final text = extractAllSseContent(body).join();
if (!looksLikeModelOutput(text)) {
throw Exception('DuckDuckGo WebView unusable: ${body.substring(0, body.length.clamp(0, 180))}');
}
return text;
} catch (e) {
if (e.toString().contains('DuckDuckGo')) rethrow;
final text = extractAllSseContent(rawResponse).join();
if (looksLikeModelOutput(text)) return text;
throw Exception('Failed to parse DuckDuckGo: $e');
}
}
}
+93
View File
@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
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';
/// ChatGPT-Next-Web / Free2GPT clones (`/api/generate` + md5 sign).
class Free2Gpt extends BaseProvider {
@override
String get name => 'Free2Gpt';
@override
bool get working => true;
@override
List<String> get models => ['gpt-3.5-turbo', 'gpt-4o-mini', 'gemini'];
@override
String get defaultModel => 'gpt-3.5-turbo';
static const _hosts = [
'https://chat.free2gpt.xyz',
'https://chatz.free2gpt.xyz',
'https://gpt.aifree.site',
'https://chat.aifree.site',
'https://gemini.free2gpt.com',
];
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
Object? last;
for (final host in _hosts) {
try {
yield await _generate(host, messages);
return;
} catch (e) {
last = e;
}
}
throw Exception('Free2Gpt failed: $last');
}
Future<String> _generate(String host, List<Message> messages) async {
final lastUser = getLastUserMessage(messages);
final t = DateTime.now().millisecondsSinceEpoch;
final sign = md5.convert(utf8.encode('$t:$lastUser:undefined')).toString();
final payload = jsonEncode({
'messages': toOpenAiMessages(messages),
'time': t,
'pass': null,
'sign': sign,
});
final urls = [
'$host/api/generate',
'$host/api/openai/v1/chat/completions',
];
Object? last;
for (final url in urls) {
try {
final req = http.Request('POST', Uri.parse(url));
req.headers.addAll(browserHeaders(
origin: host,
referer: '$host/',
contentType: 'application/json',
));
req.body = payload;
final res = await g4fHttp.send(req).timeout(const Duration(seconds: 14));
if (res.statusCode != 200) {
last = 'HTTP ${res.statusCode}';
continue;
}
final raw = utf8.decode(await res.stream.toBytes().timeout(const Duration(seconds: 14)));
final openai = extractOpenAiMessage(raw);
if (openai != null && looksLikeModelOutput(openai)) return openai.trim();
final sse = extractAllSseContent(raw).join();
if (looksLikeModelOutput(sse)) return sse.trim();
if (looksLikeModelOutput(raw)) return raw.trim();
last = 'unusable';
} catch (e) {
last = e;
}
}
throw Exception('$host failed: $last');
}
}
+164
View File
@@ -0,0 +1,164 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import 'package:uuid/uuid.dart';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'base_provider.dart';
/// z.ai GLM web chat with a guest JWT. Captcha-protected replies fail fast.
class GlmZai extends BaseProvider {
@override
String get name => 'GLM';
@override
bool get working => true;
@override
List<String> get models => ['x-preview-l', 'GLM-5-Turbo', 'glm-4.7'];
@override
String get defaultModel => 'x-preview-l';
static const _base = 'https://chat.z.ai';
static const _uuid = Uuid();
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final authRes = await g4fHttp.get(
Uri.parse('$_base/api/v1/auths/'),
headers: browserHeaders(origin: _base, referer: '$_base/', desktop: true),
).timeout(const Duration(seconds: 12));
if (authRes.statusCode != 200) {
throw Exception('GLM auth ${authRes.statusCode}');
}
final auth = jsonDecode(authRes.body) as Map<String, dynamic>;
final token = auth['token']?.toString();
final userId = auth['id']?.toString() ?? '';
if (token == null || token.isEmpty) {
throw Exception('GLM: no guest token');
}
final headers = browserHeaders(
origin: _base,
referer: '$_base/',
contentType: 'application/json',
desktop: true,
extra: {
'Authorization': 'Bearer $token',
'x-fe-version': 'prod-fe-1.1.69',
'x-region': 'overseas',
},
);
String useModel = defaultModel;
try {
final modelsRes = await g4fHttp.get(
Uri.parse('$_base/api/models'),
headers: headers,
).timeout(const Duration(seconds: 10));
if (modelsRes.statusCode == 200) {
final data = jsonDecode(modelsRes.body);
final list = (data is Map ? data['data'] : data) as List?;
if (list != null && list.isNotEmpty) {
final ids = [
for (final m in list)
if (m is Map && m['id'] != null) m['id'].toString()
];
if (ids.contains(model)) {
useModel = model;
} else if (ids.isNotEmpty) {
useModel = ids.first;
}
}
}
} catch (_) {}
final chatRes = await g4fHttp.post(
Uri.parse('$_base/api/v1/chats/new'),
headers: headers,
body: jsonEncode({
'chat': {
'id': _uuid.v4(),
'title': 'New Chat',
'models': [useModel],
'params': {},
'history': {'messages': {}, 'currentId': null},
'tags': [],
'flags': [],
'features': [],
}
}),
).timeout(const Duration(seconds: 12));
if (chatRes.statusCode != 200) {
throw Exception('GLM new chat ${chatRes.statusCode}');
}
final chatJson = jsonDecode(chatRes.body) as Map<String, dynamic>;
final chatId = (chatJson['id'] ?? (chatJson['chat'] as Map?)?['id'])?.toString();
if (chatId == null) throw Exception('GLM: no chat id');
final ts = DateTime.now().millisecondsSinceEpoch.toString();
final requestId = _uuid.v4();
final sigSrc = 'requestId=$requestId,timestamp=$ts,user_id=$userId';
final signature = Hmac(sha256, utf8.encode('')).convert(utf8.encode(sigSrc)).toString();
headers['x-signature'] = signature;
final lastUser = getLastUserMessage(messages);
final body = jsonEncode({
'stream': true,
'model': useModel,
'messages': [
{'role': 'user', 'content': lastUser}
],
'signature_prompt': lastUser.substring(0, lastUser.length.clamp(0, 500)),
'params': {},
'extra': {},
'features': {
'image_generation': false,
'web_search': false,
'auto_web_search': false,
'preview_mode': true,
'enable_thinking': false,
},
'chat_id': chatId,
'id': _uuid.v4(),
});
final url = Uri.parse(
'$_base/api/v2/chat/completions?timestamp=$ts&requestId=$requestId&user_id=$userId&version=0.0.1&platform=web&token=$token',
);
final req = http.Request('POST', url);
req.headers.addAll(headers);
req.body = body;
final res = await g4fHttp.send(req).timeout(const Duration(seconds: 30));
if (res.statusCode != 200) {
throw Exception('GLM completions ${res.statusCode}');
}
final collected = StringBuffer();
await for (final line in splitSseLines(res.stream.transform(utf8.decoder))) {
final trimmed = line.trim();
if (trimmed.isEmpty || !trimmed.startsWith('data:')) continue;
final data = trimmed.substring(5).trim();
if (data.contains('CAPTCHA') || data.contains('captcha')) {
throw Exception('GLM captcha required');
}
final content = extractSseContent(data);
if (content != null && content.isNotEmpty) {
collected.write(content);
if (stream) yield content;
}
}
final text = collected.toString().trim();
if (!looksLikeModelOutput(text)) {
throw Exception('GLM empty response');
}
if (!stream) yield text;
}
}
+114
View File
@@ -0,0 +1,114 @@
import 'dart:convert';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'webview_base_provider.dart';
/// HuggingChat (huggingface.co/chat). Guest REST is blocked; WebView carries cookies.
class HuggingChat extends WebViewBaseProvider {
@override
String get name => 'HuggingChat';
@override
bool get working => true;
@override
List<String> get models => [
'omni',
'meta-llama/Llama-3.1-8B-Instruct',
'Qwen/Qwen3.8-27B',
'zai-org/GLM-5.3-Flash',
];
@override
String get defaultModel => 'omni';
@override
String get baseUrl => 'https://huggingface.co/chat/';
@override
String get apiEndpoint => 'https://huggingface.co/chat/api/v2/conversations';
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
final promptJson = jsonEncode(formatPrompt(messages));
return '''
(async function() {
const headers = {'Content-Type': 'application/json', 'Accept': 'application/json'};
let convRes = await fetch('/chat/api/v2/conversations', {
method: 'POST',
headers,
body: JSON.stringify({ model: $modelJson })
});
if (!convRes.ok) {
convRes = await fetch('/chat/conversation', {
method: 'POST',
headers,
body: JSON.stringify({ model: $modelJson })
});
}
if (!convRes.ok) {
return JSON.stringify({error: 'HuggingChat login required: ' + convRes.status + ' ' + await convRes.text()});
}
const conv = await convRes.json();
const id = conv.json?.id || conv.id || conv.conversationId;
if (!id) return JSON.stringify({error: 'no conversation id', raw: conv});
const payload = {
id: crypto.randomUUID(),
inputs: $promptJson,
is_retry: false,
is_continue: false,
web_search: false,
tools: []
};
let msgRes = await fetch('/chat/api/v2/conversations/' + id, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'},
body: JSON.stringify(payload)
});
if (!msgRes.ok) {
msgRes = await fetch('/chat/conversation/' + id, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'},
body: JSON.stringify(payload)
});
}
const body = await msgRes.text();
return JSON.stringify({status: msgRes.status, body});
})();
''';
}
@override
String parseResponse(String rawResponse) {
try {
final json = jsonDecode(rawResponse);
if (json is Map && json['error'] != null) {
throw Exception(json['error']);
}
final body = json is Map ? (json['body']?.toString() ?? rawResponse) : rawResponse;
final sse = extractAllSseContent(body).join();
if (looksLikeModelOutput(sse)) return sse;
// chat-ui tokens: {"type":"stream","token":"..."}
final tokens = <String>[];
for (final line in body.split('\n')) {
try {
final m = jsonDecode(line);
if (m is Map) {
final token = m['token'] ?? m['text'] ?? m['generated_text'];
if (token is String && token.isNotEmpty) tokens.add(token);
}
} catch (_) {}
}
final joined = tokens.join();
if (looksLikeModelOutput(joined)) return joined;
if (looksLikeModelOutput(body)) return body.trim();
throw Exception('HuggingChat unusable: ${body.substring(0, body.length.clamp(0, 180))}');
} catch (e) {
if (looksLikeModelOutput(rawResponse)) return rawResponse.trim();
throw Exception('Failed to parse HuggingChat: $e');
}
}
}
+98
View File
@@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import 'package:uuid/uuid.dart';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'base_provider.dart';
/// Port of g4f.Provider.Liaobots — guest auth code + chat.
class Liaobots extends BaseProvider {
@override
String get name => 'Liaobots';
@override
bool get working => true;
@override
List<String> get models => ['gpt-3.5-turbo', 'gpt-4'];
@override
String get defaultModel => 'gpt-3.5-turbo';
static const _hosts = [
'https://liaobots.work',
'https://liaobots.site',
'https://liaobots.com',
];
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
Object? last;
for (final host in _hosts) {
try {
yield await _chat(host, model, messages);
return;
} catch (e) {
last = e;
}
}
throw Exception('Liaobots failed: $last');
}
Future<String> _chat(String host, String model, List<Message> messages) async {
final headers = browserHeaders(
origin: host,
referer: '$host/',
contentType: 'application/json',
);
final login = await g4fHttp
.post(
Uri.parse('$host/api/user'),
headers: headers,
body: jsonEncode({'authcode': const Uuid().v4()}),
)
.timeout(const Duration(seconds: 12));
String? auth;
if (login.statusCode == 200) {
try {
final data = jsonDecode(login.body);
auth = (data is Map ? (data['authCode'] ?? data['authcode']) : null)
?.toString();
} catch (_) {}
}
final req = http.Request('POST', Uri.parse('$host/api/chat'));
req.headers.addAll({
...headers,
if (auth != null) 'x-auth-code': auth,
if (auth != null) 'authorization': auth,
});
req.body = jsonEncode({
'conversationId': const Uuid().v4(),
'model': {
'id': models.contains(model) ? model : defaultModel,
'name': model,
'maxLength': 24000,
'tokenLimit': 8000,
},
'messages': toOpenAiMessages(messages),
'key': '',
'prompt': extractSystemMessage(messages),
});
final res = await g4fHttp.send(req).timeout(requestTimeout);
if (res.statusCode != 200) {
throw Exception('Liaobots ${res.statusCode} @ $host');
}
final raw = utf8.decode(await res.stream.toBytes().timeout(requestTimeout));
final sse = extractAllSseContent(raw).join();
final text = looksLikeModelOutput(sse) ? sse : raw.trim();
if (!looksLikeModelOutput(text)) throw Exception('Liaobots empty @ $host');
return text;
}
}
+90
View File
@@ -0,0 +1,90 @@
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';
/// OpenAI-compatible `/v1/chat/completions` client used by several free proxies.
class OpenAiCompatProvider extends BaseProvider {
OpenAiCompatProvider({
required this.providerName,
required this.apiBase,
required this.fallbackModel,
List<String>? supportedModels,
this.apiKey,
this.extraHeaders = const {},
}) : supportedModels = supportedModels ?? [fallbackModel];
final String providerName;
final String apiBase;
final String fallbackModel;
final List<String> supportedModels;
final String? apiKey;
final Map<String, String> extraHeaders;
@override
String get name => providerName;
@override
bool get working => true;
@override
List<String> get models => supportedModels;
@override
String get defaultModel => fallbackModel;
String get chatUrl {
final base = apiBase.endsWith('/') ? apiBase.substring(0, apiBase.length - 1) : apiBase;
if (base.endsWith('/chat/completions')) return base;
return '$base/chat/completions';
}
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final payload = <String, dynamic>{
'model': models.contains(model) ? model : defaultModel,
'messages': toOpenAiMessages(messages),
'stream': false,
'temperature': 0.8,
};
final headers = browserHeaders(
contentType: 'application/json',
accept: 'application/json',
extra: {
if (apiKey != null && apiKey!.isNotEmpty) 'Authorization': 'Bearer $apiKey',
...extraHeaders,
},
);
final req = http.Request('POST', Uri.parse(chatUrl));
req.headers.addAll(headers);
req.body = jsonEncode(payload);
final response = await g4fHttp.send(req).timeout(requestTimeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
final err = await response.stream.bytesToString();
throw Exception(
'$name returned ${response.statusCode}: ${err.substring(0, err.length.clamp(0, 240))}',
);
}
final body = utf8.decode(await response.stream.toBytes());
final fromJson = extractOpenAiMessage(body);
final fromSse = extractAllSseContent(body).join();
final text = (fromJson != null && looksLikeModelOutput(fromJson))
? fromJson
: fromSse;
if (!looksLikeModelOutput(text)) {
throw Exception('$name returned unusable body');
}
yield text;
}
}
+137
View File
@@ -0,0 +1,137 @@
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;
}
}
+131
View File
@@ -0,0 +1,131 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../http_client.dart';
import '../http_util.dart';
import '../models.dart';
import 'base_provider.dart';
import 'webview_base_provider.dart';
class PhindAi extends BaseProvider {
@override
String get name => 'PhindAi';
@override
String get label => 'PhindAi';
@override
String get url => 'https://phindai.org';
@override
bool get working => true;
@override
bool get supportsStream => false;
@override
Duration get requestTimeout => const Duration(seconds: 90);
@override
String get defaultModel => 'deepseek-v3';
@override
List<String> get models => ['deepseek-v3', 'deepseek'];
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
try {
yield await _rest(messages);
return;
} catch (e) {
debugPrint('Phind REST failed, trying origin WebView: $e');
}
yield await _viaWebView(messages);
}
Future<String> _rest(List<Message> messages) async {
final prompt = messages.last.content;
final headers = {
'Accept':
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'User-Agent': kDesktopUserAgent,
};
final mainPageResponse = await g4fHttp.get(Uri.parse(url), headers: headers);
if (mainPageResponse.statusCode != 200) {
throw Exception('Failed to fetch PhindAi main page: ${mainPageResponse.statusCode}');
}
final html = mainPageResponse.body;
final match = RegExp(r'"nonce":"([a-f0-9]+)"').firstMatch(html) ??
RegExp(r"'nonce':'([a-f0-9]+)'").firstMatch(html);
if (match == null) {
throw Exception('Failed to extract nonce from PhindAi response');
}
final nonce = match.group(1)!;
final response = await g4fHttp.post(
Uri.parse('$url/wp-admin/admin-ajax.php'),
headers: headers,
body: {
'action': 'phind_ai_send',
'nonce': nonce,
'message': prompt,
},
);
if (response.statusCode != 200) {
throw Exception('PhindAi API returned status: ${response.statusCode}');
}
final data = jsonDecode(response.body);
if (data is! Map || data['success'] != true) {
throw Exception('PhindAi API returned success=False: $data');
}
final responseText = (data['data'] is Map ? data['data']['response'] : null) ?? '';
final text = responseText.toString().trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Phind REST unusable');
}
return text;
}
Future<String> _viaWebView(List<Message> messages) async {
final promptJson = jsonEncode(messages.last.content);
final raw = await runOriginWebView(
originUrl: url,
timeout: requestTimeout,
functionBody: '''
const prompt = $promptJson;
const html = document.documentElement.innerHTML;
const m = html.match(/"nonce":"([a-f0-9]+)"/) || html.match(/'nonce':'([a-f0-9]+)'/);
if (!m) throw new Error('no nonce in page');
const body = new URLSearchParams();
body.set('action', 'phind_ai_send');
body.set('nonce', m[1]);
body.set('message', prompt);
const r = await fetch('/wp-admin/admin-ajax.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'same-origin',
body
});
const data = await r.json();
if (!data || data.success !== true) {
throw new Error('phind success=false ' + JSON.stringify(data).slice(0, 180));
}
return String((data.data && data.data.response) || '');
''',
);
final text = raw.trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Phind WebView unusable: ${text.substring(0, text.length.clamp(0, 180))}');
}
return text;
}
}
+124
View File
@@ -0,0 +1,124 @@
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';
class Pollinations extends BaseProvider {
@override
String get name => 'Pollinations';
@override
bool get working => true;
@override
List<String> get models => [
'openai-fast',
'openai',
'deepseek',
G4FModels.gpt4o,
G4FModels.mistral,
G4FModels.llama3,
G4FModels.qwen,
];
@override
String get defaultModel => 'openai-fast';
static const _getBase = 'https://text.pollinations.ai';
static const _postUrl = 'https://gen.pollinations.ai/v1/chat/completions';
String _resolveModel(String model) {
switch (model) {
case G4FModels.gpt4o:
case G4FModels.gpt4oMini:
case G4FModels.gpt4:
return 'openai-fast';
case G4FModels.mistral:
case G4FModels.llama3:
case G4FModels.qwen:
return 'openai-fast';
default:
return models.contains(model) ? model : defaultModel;
}
}
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final resolved = _resolveModel(model);
try {
await for (final chunk in _getPrompt(resolved, messages, stream: stream)) {
yield chunk;
}
return;
} catch (_) {
await for (final chunk in _postChat(resolved, messages, stream: stream)) {
yield chunk;
}
}
}
Stream<String> _postChat(
String model,
List<Message> messages, {
required bool stream,
}) async* {
final req = http.Request('POST', Uri.parse(_postUrl));
req.headers.addAll(browserHeaders(
contentType: 'application/json',
accept: stream ? 'text/event-stream' : 'application/json',
));
req.body = jsonEncode({
'model': model,
'messages': toOpenAiMessages(messages),
'stream': false, // anonymous POST rejects SSE
});
final response = await g4fHttp.send(req).timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
final err = await response.stream.bytesToString().timeout(const Duration(seconds: 4));
throw Exception('Pollinations POST ${response.statusCode}: $err');
}
final body = utf8.decode(await response.stream.toBytes().timeout(const Duration(seconds: 30)));
if (looksLikeGarbage(body)) {
throw Exception('Pollinations POST garbage');
}
final text = extractOpenAiMessage(body) ?? body.trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Pollinations POST empty');
}
yield text;
}
Stream<String> _getPrompt(
String model,
List<Message> messages, {
required bool stream,
}) async* {
final seed = DateTime.now().millisecondsSinceEpoch % 100000;
final prompt = formatPrompt(messages);
final url = Uri.parse(
'$_getBase/${Uri.encodeComponent(prompt)}?model=$model&seed=$seed',
);
final req = http.Request('GET', url);
req.headers.addAll(browserHeaders());
final response = await g4fHttp.send(req).timeout(const Duration(seconds: 30));
if (response.statusCode != 200) {
throw Exception('Pollinations returned status ${response.statusCode}');
}
final fullText = utf8
.decode(await response.stream.toBytes().timeout(const Duration(seconds: 30)))
.trim();
if (!looksLikeModelOutput(fullText)) {
throw Exception('Pollinations GET empty');
}
yield fullText;
}
}
+81
View File
@@ -0,0 +1,81 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import 'package:uuid/uuid.dart';
import '../http_util.dart';
import '../models.dart';
import 'base_provider.dart';
/// Port of g4f.Provider.Quillbot — full JSON-lines reply, no UI stream.
class Quillbot extends BaseProvider {
@override
String get name => 'Quillbot';
@override
bool get working => true;
@override
List<String> get models => ['quillbot', 'quillbot-search'];
@override
String get defaultModel => 'quillbot';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final conversationId = const Uuid().v4();
final url =
'https://quillbot.com/api/ai-chat/chat/conversation/$conversationId';
final payload = {
'message': {'content': '${formatPrompt(messages)}\n\n'},
'context': {
'editorContext': '',
'selectionContext': '',
'userDialect': 'en-us',
'apiVersion': 2,
},
'origin': {'name': 'ai-chat.chat', 'url': 'https://quillbot.com'},
if (model.contains('search')) 'tools': {'web_search_builtin': {}},
};
final req = http.Request('POST', Uri.parse(url));
req.headers.addAll(browserHeaders(
origin: 'https://quillbot.com',
referer: 'https://quillbot.com/ai-chat/c/$conversationId',
contentType: 'application/json',
desktop: true,
extra: {
'accept': 'text/event-stream',
'platform-type': 'webapp',
'qb-product': 'AI-CHAT',
'useridtoken': 'empty-token',
'webapp-version': '42.61.1',
},
));
req.body = jsonEncode(payload);
final res = await g4fHttp.send(req).timeout(requestTimeout);
if (res.statusCode != 200) {
throw Exception('Quillbot ${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 && data['type'] == 'content' && data['content'] is String) {
buf.write(data['content']);
}
} catch (_) {}
}
final text = buf.toString().trim();
if (!looksLikeModelOutput(text)) {
throw Exception('Quillbot empty');
}
yield text;
}
}
+109
View File
@@ -0,0 +1,109 @@
import 'dart:convert';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'webview_base_provider.dart';
/// Qwen chat (chat.qwen.ai). WAF is solved inside the hidden WebView.
class QwenChat extends WebViewBaseProvider {
@override
String get name => 'Qwen';
@override
bool get working => true;
@override
List<String> get models => ['qwen3.5-flash', 'qwen3.7-plus', 'qwen3-max-2026-01-23'];
@override
String get defaultModel => 'qwen3.5-flash';
@override
String get baseUrl => 'https://chat.qwen.ai/';
@override
String get apiEndpoint => 'https://chat.qwen.ai/api/v2/chat/completions';
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
final promptJson = jsonEncode(getLastUserMessage(messages));
return '''
(async function() {
for (let i = 0; i < 16; i++) {
if (!document.documentElement.innerHTML.includes('aliyun_waf_aa')) break;
await new Promise(r => setTimeout(r, 400));
}
const headers = {'Content-Type': 'application/json', 'source': 'web'};
const created = await fetch('/api/v2/chats/new', {
method: 'POST',
headers,
body: JSON.stringify({
title: 'New Chat',
models: [$modelJson],
chat_mode: 'normal',
chat_type: 't2t',
timestamp: Date.now()
})
});
const createdJson = await created.json();
const chatId = createdJson.data && createdJson.data.id;
if (!chatId) return JSON.stringify({error: createdJson});
const resp = await fetch('/api/v2/chat/completions?chat_id=' + chatId, {
method: 'POST',
headers,
body: JSON.stringify({
stream: false,
version: '2.1',
incremental_output: false,
chat_id: chatId,
chat_mode: 'normal',
model: $modelJson,
messages: [{
fid: crypto.randomUUID(),
role: 'user',
content: $promptJson,
user_action: 'chat',
files: [],
timestamp: Date.now(),
models: [$modelJson],
chat_type: 't2t',
extra: {meta: {subChatType: 't2t'}},
sub_chat_type: 't2t'
}],
timestamp: Date.now()
})
});
return await resp.text();
})();
''';
}
@override
String parseResponse(String rawResponse) {
final sse = extractAllSseContent(rawResponse).join();
if (looksLikeModelOutput(sse)) return sse;
try {
final json = jsonDecode(rawResponse);
if (json is Map) {
if (json['error'] != null) throw Exception(json['error']);
final choices = json['choices'];
if (choices is List && choices.isNotEmpty) {
final c = choices.first;
if (c is Map) {
final msg = c['message'];
if (msg is Map && msg['content'] is String) return msg['content'];
final delta = c['delta'];
if (delta is Map && delta['content'] is String) return delta['content'];
}
}
final data = json['data'];
if (data is Map && data['content'] is String) return data['content'];
}
} catch (e) {
if (e.toString().startsWith('Exception:')) rethrow;
}
if (looksLikeModelOutput(rawResponse)) return rawResponse.trim();
throw Exception('Failed to parse Qwen: ${rawResponse.substring(0, rawResponse.length.clamp(0, 180))}');
}
}
+94
View File
@@ -0,0 +1,94 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import '../http_util.dart';
import '../models.dart';
import 'base_provider.dart';
import 'webview_base_provider.dart';
class TeachAnything extends BaseProvider {
@override
String get name => 'TeachAnything';
@override
String get url => 'https://www.teach-anything.com';
@override
bool get working => true;
@override
Duration get requestTimeout => const Duration(seconds: 90);
@override
List<String> get models => ['gemma'];
@override
String get defaultModel => 'gemma';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
try {
yield await _rest(messages);
return;
} catch (e) {
debugPrint('TeachAnything REST failed, trying origin WebView: $e');
}
yield await _viaWebView(messages);
}
Future<String> _rest(List<Message> messages) async {
final prompt = formatPrompt(messages);
final req = http.Request('POST', Uri.parse('$url/api/generate'));
req.headers.addAll({
'accept': '*/*',
'accept-language': 'en-US,en;q=0.9',
'cache-control': 'no-cache',
'content-type': 'application/json',
'origin': url,
'referer': '$url/',
'user-agent': kDesktopUserAgent,
});
req.body = jsonEncode({'prompt': prompt});
final response = await g4fHttp.send(req).timeout(requestTimeout);
if (response.statusCode != 200) {
throw Exception('$name returned status ${response.statusCode}');
}
final fullText = utf8.decode(await response.stream.toBytes()).trim();
if (!looksLikeModelOutput(fullText)) throw Exception('$name empty');
return fullText;
}
Future<String> _viaWebView(List<Message> messages) async {
final promptJson = jsonEncode(formatPrompt(messages));
final raw = await runOriginWebView(
originUrl: url,
timeout: requestTimeout,
functionBody: '''
const prompt = $promptJson;
const r = await fetch('/api/generate', {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': '*/*' },
credentials: 'same-origin',
body: JSON.stringify({ prompt })
});
if (!r.ok) throw new Error('status ' + r.status);
return await r.text();
''',
);
final text = raw.trim();
if (!looksLikeModelOutput(text)) {
throw Exception(
'TeachAnything WebView unusable: ${text.substring(0, text.length.clamp(0, 180))}',
);
}
return text;
}
}
@@ -0,0 +1,88 @@
import 'dart:convert';
import '../../models.dart';
import '../webview_base_provider.dart';
class DeepInfraWebView extends WebViewBaseProvider {
@override
String get name => 'DeepInfra';
@override
bool get working => true;
@override
List<String> get models => ['meta-llama/Meta-Llama-3-70B-Instruct', 'Qwen/Qwen2-72B-Instruct'];
@override
String get defaultModel => 'meta-llama/Meta-Llama-3-70B-Instruct';
@override
String get baseUrl => 'https://deepinfra.com/chat';
@override
String get apiEndpoint => 'https://api.deepinfra.com/v1/openai/chat/completions';
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
// 1. We extract the Turnstile token from the DOM
// 2. We make a fetch POST to the API endpoint
// 3. We return the JSON stringified response back to Dart
final messagesJson = jsonEncode(messages.map((m) => m.toJson()).toList());
return '''
(async function() {
// Wait up to 10 seconds for Turnstile token
let token = '';
for (let i = 0; i < 20; i++) {
const input = document.querySelector('[name=cf-turnstile-response]');
if (input && input.value) {
token = input.value;
break;
}
await new Promise(r => setTimeout(r, 500));
}
if (!token) {
return JSON.stringify({error: "Turnstile token not found"});
}
const payload = {
model: "$model",
messages: $messagesJson,
stream: false
};
try {
const response = await fetch("$apiEndpoint", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Deepinfra-Source': 'web-page',
'X-Deepinfra-Turnstile': token
},
body: JSON.stringify(payload)
});
const data = await response.json();
return JSON.stringify(data);
} catch (e) {
return JSON.stringify({error: e.toString()});
}
})();
''';
}
@override
String parseResponse(String rawResponse) {
try {
final json = jsonDecode(rawResponse);
if (json['error'] != null) {
throw Exception(json['error']);
}
return json['choices'][0]['message']['content'];
} catch (e) {
throw Exception('Failed to parse DeepInfra response: $rawResponse | Error: $e');
}
}
}
@@ -0,0 +1,206 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../http_client.dart';
import '../http_util.dart';
import '../models.dart';
import 'base_provider.dart';
/// Cookie to plant on the origin before the page loads (Python fake Copilot cookies).
class CookieSeed {
const CookieSeed({
required this.name,
required this.value,
this.domain,
this.path = '/',
this.isSecure = true,
});
final String name;
final String value;
final String? domain;
final String path;
final bool isSecure;
}
/// Load [originUrl] in a real Chromium WebView and run async JS on that origin.
///
/// This is the Android analog of g4f nodriver: same-origin `fetch` / WebSocket
/// after Cloudflare, not a generic fetch from about:blank (CORS → `{}`).
///
/// `evaluateJavascript` does not await Promises on Android and stringifies
/// them as `{}`. Always use `callAsyncJavaScript`.
Future<String> runOriginWebView({
required String originUrl,
required String functionBody,
Duration timeout = const Duration(seconds: 70),
List<CookieSeed> seeds = const [],
}) async {
if (kIsWeb ||
(!Platform.isAndroid &&
!Platform.isIOS &&
!Platform.isMacOS &&
!Platform.isWindows)) {
throw UnsupportedError('Origin WebView is not supported on this platform.');
}
final originUri = Uri.parse(originUrl);
final cookieManager = CookieManager.instance();
for (final seed in seeds) {
try {
await cookieManager.setCookie(
url: WebUri(originUrl),
name: seed.name,
value: seed.value,
domain: seed.domain ?? originUri.host,
path: seed.path,
isSecure: seed.isSecure,
);
} catch (e) {
debugPrint('WebView setCookie ${seed.name}: $e');
}
}
final completer = Completer<String>();
var started = false;
HeadlessInAppWebView? view;
Future<void> importCookies() async {
try {
final cookies = await cookieManager.getCookies(url: WebUri(originUrl));
for (final c in cookies) {
g4fCookies.set(
name: c.name,
value: '${c.value}',
domain: c.domain ?? originUri.host,
path: c.path ?? '/',
secure: c.isSecure ?? originUri.scheme == 'https',
hostOnly: c.domain == null || c.domain == originUri.host,
);
}
} catch (e) {
debugPrint('WebView import cookies: $e');
}
}
Future<void> runScript(
InAppWebViewController controller, {
bool force = false,
}) async {
if (started || completer.isCompleted) return;
final title = (await controller.getTitle() ?? '').toLowerCase();
final challenge = title.contains('just a moment') ||
title.contains('attention required') ||
title.contains('cloudflare');
if (challenge && !force) return;
started = true;
try {
await Future<void>.delayed(const Duration(milliseconds: 800));
await importCookies();
final result = await controller.callAsyncJavaScript(
functionBody: functionBody,
);
if (result?.error != null && result!.error!.trim().isNotEmpty) {
throw Exception(result.error);
}
final value = result?.value;
if (value == null) {
throw Exception('WebView script returned null');
}
final raw = value is String ? value : jsonEncode(value);
if (!completer.isCompleted) completer.complete(raw);
} catch (e) {
if (!completer.isCompleted) completer.completeError(e);
}
}
view = HeadlessInAppWebView(
initialUrlRequest: URLRequest(url: WebUri(originUrl)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
domStorageEnabled: true,
thirdPartyCookiesEnabled: true,
incognito: false,
userAgent: kDesktopUserAgent,
isFraudulentWebsiteWarningEnabled: false,
cacheEnabled: true,
),
onLoadStop: (controller, url) async {
await runScript(controller);
},
onConsoleMessage: (controller, consoleMessage) {
debugPrint('WebView Console: ${consoleMessage.message}');
},
);
await view.run();
final ticker = Timer.periodic(const Duration(seconds: 3), (t) async {
if (completer.isCompleted) {
t.cancel();
return;
}
final controller = view?.webViewController;
if (controller != null) {
await runScript(controller, force: t.tick >= 5);
}
});
try {
return await completer.future.timeout(timeout);
} finally {
ticker.cancel();
await importCookies();
await view.dispose();
}
}
String wrapIifeAsFunctionBody(String iife) {
return '''
const __out = await $iife;
if (__out == null) return '';
if (typeof __out === 'string') return __out;
try { return JSON.stringify(__out); } catch (e) { return String(__out); }
''';
}
abstract class WebViewBaseProvider extends BaseProvider {
@override
bool get isWebView => true;
@override
Duration get requestTimeout => const Duration(seconds: 90);
/// The origin to load so Cloudflare / same-origin fetch can run.
String get baseUrl;
/// The specific API endpoint to call via JS `fetch`.
String get apiEndpoint;
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final raw = await runOriginWebView(
originUrl: baseUrl,
functionBody: wrapIifeAsFunctionBody(
buildJsFetchCode(model, messages, stream),
),
timeout: requestTimeout,
);
yield parseResponse(raw);
}
/// Builds an IIFE that returns the API body (string or JSON).
String buildJsFetchCode(String model, List<Message> messages, bool stream);
/// Parses the raw string returned by the JS code into the final AI answer.
String parseResponse(String rawResponse);
}
+74
View File
@@ -0,0 +1,74 @@
import 'dart:convert';
import '../http_client.dart';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'base_provider.dart';
/// Port of g4f.Provider.You (default youchat mode, no login).
class YouChat extends BaseProvider {
@override
String get name => 'You';
@override
bool get working => true;
@override
List<String> get models => ['gpt-3.5-turbo', 'gpt-4o'];
@override
String get defaultModel => 'gpt-3.5-turbo';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final res = await g4fHttp
.post(
Uri.parse('https://you.com/api/streamingSearch'),
headers: browserHeaders(
origin: 'https://you.com',
referer: 'https://you.com/search?fromSearchBar=true&tbm=youchat',
accept: 'text/event-stream',
),
body: {
'q': formatPrompt(messages),
'domain': 'youchat',
'selectedChatMode': 'default',
'chat': '[]',
},
)
.timeout(requestTimeout);
if (res.statusCode != 200) {
throw Exception('You ${res.statusCode}');
}
final raw = res.body;
final buf = StringBuffer();
String? event;
for (final line in raw.split(RegExp(r'\r?\n'))) {
if (line.startsWith('event:')) {
event = line.substring(6).trim();
} else if (line.startsWith('data:')) {
final dataStr = line.substring(5).trim();
try {
final data = jsonDecode(dataStr);
if (data is Map) {
if (event == 'youChatToken' && data['youChatToken'] is String) {
buf.write(data['youChatToken']);
} else if (event == 'youChatUpdate' && data['t'] is String) {
buf.write(data['t']);
}
}
} catch (_) {}
}
}
var text = buf.toString().trim();
if (!looksLikeModelOutput(text)) {
text = extractAllSseContent(raw).join().trim();
}
if (!looksLikeModelOutput(text)) throw Exception('You empty');
yield text;
}
}
+76
View File
@@ -0,0 +1,76 @@
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';
/// Faithful port of `g4f.Provider.Yqcloud`.
class Yqcloud extends BaseProvider {
@override
String get name => 'Yqcloud';
@override
String get url => 'https://chat9.yqcloud.top';
@override
bool get working => true;
@override
List<String> get models => const ['gpt-4'];
@override
String get defaultModel => 'gpt-4';
@override
Map<String, String> get modelAliases => const {
G4FModels.gpt4o: 'gpt-4',
G4FModels.gpt4oMini: 'gpt-4',
};
static const _endpoint = 'https://api.binjie.fun/api/generateStream';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
final systemMessage = extractSystemMessage(messages);
final history = [
for (final m in messages)
if (m.role != Role.system) m,
];
final prompt = formatPrompt(history, includeSystem: false);
final userId = '#/chat/${DateTime.now().millisecondsSinceEpoch}';
final req = http.Request('POST', Uri.parse(_endpoint));
req.headers.addAll({
'accept': 'application/json, text/plain, */*',
'accept-language': 'en-US,en;q=0.9',
'content-type': 'application/json',
'origin': url,
'referer': '$url/',
'user-agent': kDesktopUserAgent,
});
req.body = jsonEncode({
'prompt': prompt,
'userId': userId,
'network': true,
'system': systemMessage,
'withoutContext': false,
'stream': true,
});
final response = await g4fHttp.send(req).timeout(requestTimeout);
if (response.statusCode != 200) {
final err = await response.stream.bytesToString();
throw Exception('$name returned ${response.statusCode}: $err');
}
final fullText = utf8.decode(await response.stream.toBytes()).trim();
if (!looksLikeModelOutput(fullText) || fullText.contains('Error:')) {
throw Exception('$name unusable body');
}
yield fullText;
}
}
+47
View File
@@ -0,0 +1,47 @@
import 'http_client.dart';
import 'http_util.dart';
import 'models.dart';
import 'providers/base_provider.dart';
/// Port of g4f `IterListProvider`: try each provider until one returns content.
class IterListProvider {
IterListProvider(this.providers);
final List<BaseProvider> providers;
Future<String> create({
required List<Message> messages,
String? model,
}) async {
final exceptions = <String, Object>{};
final ordered = [
...providers.where((p) => p.working && !p.isWebView),
...providers.where((p) => p.working && p.isWebView),
];
for (final provider in ordered) {
final alias = provider.resolveModel(model);
try {
await warmupOrigin(provider.url);
final result = await provider.complete(alias, messages);
if (looksLikeModelOutput(result)) {
return result.trim();
}
exceptions[provider.name] = 'unusable text (${result.trim().length} chars)';
} catch (e) {
exceptions[provider.name] = e;
print('Provider ${provider.name} failed: $e');
}
}
if (exceptions.isEmpty) {
throw Exception('No G4F providers available.');
}
if (exceptions.length == 1) {
throw Exception('${exceptions.keys.first}: ${exceptions.values.first}');
}
throw Exception(
'RetryProvider failed:\n${exceptions.entries.map((e) => '${e.key}: ${e.value}').join('\n')}',
);
}
}
+150
View File
@@ -0,0 +1,150 @@
import 'dart:convert';
/// Reassembles SSE / text lines that TCP may split across chunks.
Stream<String> splitSseLines(Stream<String> input) async* {
final buf = StringBuffer();
await for (final chunk in input) {
buf.write(chunk);
var text = buf.toString();
final parts = text.split('\n');
buf
..clear()
..write(parts.removeLast());
for (final part in parts) {
yield part.endsWith('\r') ? part.substring(0, part.length - 1) : part;
}
}
if (buf.isNotEmpty) {
final rest = buf.toString();
yield rest.endsWith('\r') ? rest.substring(0, rest.length - 1) : rest;
}
}
/// Pulls visible assistant text out of one SSE `data:` payload.
String? extractSseContent(String data) {
final trimmed = data.trim();
if (trimmed.isEmpty || trimmed == '[DONE]' || trimmed == 'null') return null;
if (trimmed.startsWith('[DONE]')) return null;
dynamic decoded;
try {
decoded = jsonDecode(trimmed);
} catch (_) {
// Plain-text SSE (Yqcloud / TeachAnything style).
if (looksLikeJson(trimmed)) return null;
return trimmed;
}
if (decoded is String) {
return decoded.isEmpty ? null : decoded;
}
if (decoded is! Map) return null;
final map = Map<String, dynamic>.from(decoded);
if (map['action'] == 'error' || map['error'] != null) {
final err = map['error'] ?? map['type'] ?? map['message'];
throw Exception('SSE error: $err');
}
// Duck.ai
final duck = map['message'];
if (duck is String && duck.isNotEmpty && map['role'] != 'user') {
return duck;
}
// OpenAI chat.completions streaming
final choices = map['choices'];
if (choices is List && choices.isNotEmpty) {
final choice = choices.first;
if (choice is Map) {
final delta = choice['delta'];
if (delta is Map) {
final content = delta['content'];
if (content is String && content.isNotEmpty) return content;
}
final message = choice['message'];
if (message is Map) {
final content = message['content'];
if (content is String && content.isNotEmpty) return content;
}
final text = choice['text'];
if (text is String && text.isNotEmpty) return text;
}
}
// GLM / z.ai
if (map['type'] == 'chat:completion') {
final inner = map['data'];
if (inner is Map) {
final err = inner['error'];
if (err != null) {
throw Exception('SSE error: $err');
}
final phase = inner['phase'];
if (phase == 'thinking') return null;
final delta = inner['delta_content'];
if (delta is String && delta.isNotEmpty && phase != 'thinking') {
return delta;
}
final edit = inner['edit_content'];
if (edit is String && edit.isNotEmpty) {
return edit.split('\n</details>\n').last;
}
}
}
final content = map['content'];
if (content is String && content.isNotEmpty) return content;
return null;
}
String? extractOpenAiMessage(String body) {
final trimmed = body.trim();
if (trimmed.isEmpty) return null;
try {
final decoded = jsonDecode(trimmed);
if (decoded is Map) {
if (decoded['error'] != null) {
throw Exception('API error: ${decoded['error']}');
}
final choices = decoded['choices'];
if (choices is List && choices.isNotEmpty) {
final choice = choices.first;
if (choice is Map) {
final message = choice['message'];
if (message is Map && message['content'] is String) {
return message['content'] as String;
}
if (choice['text'] is String) return choice['text'] as String;
}
}
if (decoded['output_text'] is String) {
return decoded['output_text'] as String;
}
}
} catch (e) {
if (e.toString().startsWith('Exception: API error')) rethrow;
}
return null;
}
bool looksLikeJson(String text) {
final t = text.trimLeft();
return t.startsWith('{') || t.startsWith('[');
}
/// Parses a full SSE document (e.g. WebView returning response.text()).
Iterable<String> extractAllSseContent(String raw) sync* {
for (final line in raw.split(RegExp(r'\r?\n'))) {
final trimmed = line.trim();
if (trimmed.isEmpty) continue;
if (trimmed.startsWith('event:')) continue;
String data = trimmed;
if (trimmed.startsWith('data:')) {
data = trimmed.substring(5).trim();
}
final content = extractSseContent(data);
if (content != null && content.isNotEmpty) yield content;
}
}
+145 -1
View File
@@ -2,9 +2,20 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'screens/history_screen.dart';
import 'screens/home_search_screen.dart';
import 'screens/oracle_chat_screen.dart';
import 'screens/provider_bakeoff_screen.dart';
import 'services/words_repository.dart';
import 'widgets/gradient_star_badge.dart';
import 'g4f/client.dart';
import 'g4f/models.dart';
import 'g4f/providers/chatgpt_oss.dart';
import 'g4f/providers/pollinations.dart';
import 'g4f/providers/teach_anything.dart';
import 'g4f/providers/yqcloud.dart';
import 'g4f/providers/deep_ai.dart';
import 'g4f/providers/phind_ai.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await WordsRepository().init();
@@ -73,7 +84,9 @@ class _Words999AppState extends State<Words999App> {
}),
),
),
home: const MainNavigationContainer(),
home: const bool.fromEnvironment('ORACLE_BAKEOFF')
? const ProviderBakeoffScreen()
: const MainNavigationContainer(),
);
}
}
@@ -119,6 +132,137 @@ class _MainNavigationContainerState extends State<MainNavigationContainer> {
),
],
),
floatingActionButton: GestureDetector(
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ProviderBakeoffScreen()),
);
},
child: FloatingActionButton.extended(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const OracleChatScreen()),
);
},
icon: const Icon(Icons.auto_awesome),
label: const Text("ОРАКУЛ"),
),
),
);
}
}
class _G4FTestHelper {
Future<String> testAI() async {
final client = G4FClient();
// We access the providers from the client's internal list by just creating them here to test directly.
// Or simply test the main client logic. To test all, we will test them individually.
// We import them dynamically inside the test to keep main.dart clean long-term
final results = <String>[];
final messages = [Message(role: Role.user, content: "Коротко ответь 'Работает' и добавь одно случайное эмодзи.")];
// Yqcloud
try {
print("⏳ Тестируем Yqcloud...");
results.add("📡 Yqcloud: " + await _testProvider(client, "Yqcloud", messages));
print("✅ Yqcloud ответил!");
} catch (e) {
print("❌ Yqcloud Ошибка: $e");
results.add("❌ Yqcloud Ошибка: $e");
}
// TeachAnything
try {
print("⏳ Тестируем TeachAnything...");
results.add("📡 TeachAnything: " + await _testProvider(client, "TeachAnything", messages));
print("✅ TeachAnything ответил!");
} catch (e) {
print("❌ TeachAnything Ошибка: $e");
results.add("❌ TeachAnything Ошибка: $e");
}
// ChatGptOss
try {
print("⏳ Тестируем ChatGptOss...");
results.add("📡 ChatGptOss: " + await _testProvider(client, "ChatGptOss", messages));
print("✅ ChatGptOss ответил!");
} catch (e) {
print("❌ ChatGptOss Ошибка: $e");
results.add("❌ ChatGptOss Ошибка: $e");
}
// Pollinations
try {
print("⏳ Тестируем Pollinations...");
results.add("📡 Pollinations: " + await _testProvider(client, "Pollinations", messages));
print("✅ Pollinations ответил!");
} catch (e) {
print("❌ Pollinations Ошибка: $e");
results.add("❌ Pollinations Ошибка: $e");
}
// DeepAi
try {
print("⏳ Тестируем DeepAi...");
results.add("📡 DeepAi: " + await _testProvider(client, "DeepAi", messages));
print("✅ DeepAi ответил!");
} catch (e) {
print("❌ DeepAi Ошибка: $e");
results.add("❌ DeepAi Ошибка: $e");
}
// PhindAi
try {
print("⏳ Тестируем PhindAi...");
results.add("📡 PhindAi: " + await _testProvider(client, "PhindAi", messages));
print("✅ PhindAi ответил!");
} catch (e) {
print("❌ PhindAi Ошибка: $e");
results.add("❌ PhindAi Ошибка: $e");
}
// DeepInfraWebView
try {
print("⏳ Тестируем DeepInfraWebView (в скрытом браузере)...");
String finalResult = "";
await for (final chunk in client.chat.completions.createStream(messages: messages)) {
finalResult += chunk;
}
results.add("📡 DeepInfraWebView: $finalResult");
print("✅ DeepInfraWebView ответил!");
} catch (e) {
print("❌ DeepInfraWebView Ошибка: $e");
results.add("❌ DeepInfraWebView Ошибка: $e");
}
print("🎉 Все тесты завершены!");
return results.join("\\n\\n");
}
Future<String> _testProvider(G4FClient client, String providerName, List<Message> messages) async {
try {
dynamic provider;
switch (providerName) {
case "Yqcloud": provider = Yqcloud(); break;
case "TeachAnything": provider = TeachAnything(); break;
case "ChatGptOss": provider = ChatGptOss(); break;
case "Pollinations": provider = Pollinations(); break;
case "DeepAi": provider = DeepAi(); break;
case "PhindAi": provider = PhindAi(); break;
}
final stream = provider.createAsyncGenerator(
provider.defaultModel,
messages,
stream: false,
);
return await stream.join();
} catch (e) {
return "ОШИБКА: $e";
}
}
}
+267
View File
@@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'dart:ui';
import '../g4f/client.dart';
import '../g4f/models.dart';
class OracleChatScreen extends StatefulWidget {
const OracleChatScreen({Key? key}) : super(key: key);
@override
State<OracleChatScreen> createState() => _OracleChatScreenState();
}
class ChatMessage {
final String text;
final bool isUser;
ChatMessage({required this.text, required this.isUser});
}
class _OracleChatScreenState extends State<OracleChatScreen> {
final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
final G4FClient _client = G4FClient();
final List<ChatMessage> _messages = [
ChatMessage(
text: "Я Оракул. Мой взор проникает сквозь завесу времени. О чем ты хочешь спросить?",
isUser: false
),
];
bool _isLoading = false;
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
static const _oracleSystem = Message(
role: Role.system,
content:
'Ты — Великий Оракул 999, древний архетипический разум. '
'Стиль речи: глубокий, философский, лаконичный, без банальностей. '
'Отвечай на языке вопрошающего. Не упоминай модели, провайдеров и технические сбои.',
);
Future<void> _sendMessage() async {
final text = _controller.text.trim();
if (text.isEmpty || _isLoading) return;
setState(() {
_messages.add(ChatMessage(text: text, isUser: true));
_messages.add(ChatMessage(text: '', isUser: false));
_isLoading = true;
});
_controller.clear();
_scrollToBottom();
final apiMessages = <Message>[
_oracleSystem,
for (final m in _messages)
if (m.text.trim().isNotEmpty)
Message(
role: m.isUser ? Role.user : Role.assistant,
content: m.text,
),
];
try {
final reply = await _client.chat.completions.create(
model: G4FModels.gpt4o,
messages: apiMessages,
);
if (!mounted) return;
_setLastOracleText(
reply.trim().isEmpty
? 'Мой взор затуманен. Знаки молчат. Спроси ещё раз.'
: reply.trim(),
);
} catch (_) {
if (!mounted) return;
_setLastOracleText('Мой взор затуманен. Связь прервалась.');
} finally {
if (mounted) {
setState(() => _isLoading = false);
_scrollToBottom();
}
}
}
void _setLastOracleText(String text) {
setState(() {
_messages[_messages.length - 1] = ChatMessage(text: text, isUser: false);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF060B19),
appBar: AppBar(
title: const Text("Оракул", style: TextStyle(color: Color(0xFFFFD700), fontWeight: FontWeight.w300, letterSpacing: 2.0)),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Color(0xFFFFD700)),
),
extendBodyBehindAppBar: true,
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF0D0415), Color(0xFF060B19)],
),
),
child: SafeArea(
child: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
itemCount: _messages.length,
itemBuilder: (context, index) {
final msg = _messages[index];
return _buildMessageBubble(msg);
},
),
),
if (_isLoading)
const Padding(
padding: EdgeInsets.all(8.0),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Color(0xFFFFD700),
strokeWidth: 2,
),
),
),
_buildInputArea(),
],
),
),
),
);
}
Widget _buildMessageBubble(ChatMessage message) {
return Align(
alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 16.0),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.8),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: Radius.circular(message.isUser ? 20 : 5),
bottomRight: Radius.circular(message.isUser ? 5 : 20),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(
color: message.isUser
? const Color(0xFF1E293B).withOpacity(0.6)
: const Color(0xFF2E1A47).withOpacity(0.4),
border: Border.all(
color: message.isUser
? Colors.white.withOpacity(0.1)
: const Color(0xFFFFD700).withOpacity(0.2),
width: 1,
),
),
child: Text(
message.text,
style: TextStyle(
color: message.isUser ? Colors.white : const Color(0xFFE5E7EB),
fontSize: 16,
height: 1.4,
),
),
),
),
),
),
);
}
Widget _buildInputArea() {
return Container(
padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 24),
decoration: BoxDecoration(
color: const Color(0xFF0D0415).withOpacity(0.8),
border: Border(
top: BorderSide(
color: const Color(0xFFFFD700).withOpacity(0.2),
width: 1,
),
),
),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: TextField(
controller: _controller,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: "Спроси Оракула...",
hintStyle: TextStyle(color: Colors.white38),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 14),
),
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
),
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: _isLoading ? null : _sendMessage,
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const RadialGradient(
colors: [Color(0xFFFFD700), Color(0xFFB8860B)],
center: Alignment.topLeft,
radius: 1.0,
),
boxShadow: [
BoxShadow(
color: const Color(0xFFFFD700).withOpacity(0.3),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: const Icon(
Icons.auto_awesome,
color: Color(0xFF2E1A47),
size: 24,
),
),
),
],
),
);
}
}
+338
View File
@@ -0,0 +1,338 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import '../data/words_data.dart';
import '../g4f/g4f.dart';
import '../models/history_entry_model.dart';
import '../models/word_model.dart';
import '../services/ai_oracle_service.dart';
class ProviderProbeRow {
ProviderProbeRow({
required this.provider,
this.running = false,
this.ok,
this.text = '',
this.error,
this.elapsedMs = 0,
});
final BaseProvider provider;
bool running;
bool? ok;
String text;
String? error;
int elapsedMs;
String get name => provider.name;
bool get isWebView => provider.isWebView;
}
/// On-device comparison: the real Oracle prompt, every G4F provider, Android process.
class ProviderBakeoffScreen extends StatefulWidget {
const ProviderBakeoffScreen({super.key, this.autoStart = true});
final bool autoStart;
@override
State<ProviderBakeoffScreen> createState() => _ProviderBakeoffScreenState();
}
class _ProviderBakeoffScreenState extends State<ProviderBakeoffScreen> {
static const _question = 'Что скрыто в моём нынешнем выборе?';
late final List<SessionWord> _words;
late final List<Message> _messages;
late final List<ProviderProbeRow> _rows;
bool _running = false;
bool _done = false;
@override
void initState() {
super.initState();
_words = _pickWords();
_messages = AiOracleService().buildInterpretationMessages(
words: _words,
question: _question,
);
_rows = [
for (final p in G4FClient().chat.completions.providers)
if (p.working) ProviderProbeRow(provider: p),
];
if (widget.autoStart) {
WidgetsBinding.instance.addPostFrameCallback((_) => _run());
}
}
List<SessionWord> _pickWords() {
WordItem byName(String name) => kRussianTop1000Words.firstWhere(
(w) => w.word == name,
);
final items = [
byName('огонь').copyWith(sessionNumber: 17),
byName('зеркало').copyWith(sessionNumber: 92),
byName('порог').copyWith(sessionNumber: 112),
];
return [
for (final item in items) SessionWord.fromWordItem(item),
];
}
Future<void> _run() async {
if (_running) return;
setState(() {
_running = true;
_done = false;
for (final row in _rows) {
row.running = false;
row.ok = null;
row.text = '';
row.error = null;
row.elapsedMs = 0;
}
});
_log('BEGIN question=$_question words=${_words.map((w) => w.word).join(",")}');
for (final row in _rows) {
if (!mounted) return;
setState(() => row.running = true);
_log('START ${row.name} webview=${row.isWebView}');
final sw = Stopwatch()..start();
try {
final model = row.provider.models.contains(G4FModels.gpt4o)
? G4FModels.gpt4o
: row.provider.defaultModel;
final text = await row.provider.complete(model, _messages);
sw.stop();
final ok = looksLikeModelOutput(text) && text.trim().length >= 2;
row
..ok = ok
..text = text.trim()
..error = ok ? null : 'unusable'
..elapsedMs = sw.elapsedMilliseconds
..running = false;
_log(
'DONE ${row.name} ok=$ok ms=${row.elapsedMs} chars=${row.text.length}',
);
_log('TEXT ${row.name} ${jsonEncode(row.text)}');
} catch (e) {
sw.stop();
row
..ok = false
..error = e.toString()
..elapsedMs = sw.elapsedMilliseconds
..running = false;
_log('FAIL ${row.name} ms=${row.elapsedMs} $e');
}
if (mounted) setState(() {});
}
await _writeReport();
_log('END ok=${_rows.where((r) => r.ok == true).length}/${_rows.length}');
if (mounted) {
setState(() {
_running = false;
_done = true;
});
}
}
Future<void> _writeReport() async {
final report = {
'platform': Platform.operatingSystem,
'question': _question,
'words': [
for (final w in _words)
{
'word': w.word,
'sessionNumber': w.sessionNumber,
'category': w.category,
'symbolicMeaning': w.symbolicMeaning,
}
],
'results': [
for (final r in _rows)
{
'name': r.name,
'webview': r.isWebView,
'ok': r.ok,
'ms': r.elapsedMs,
'error': r.error,
'text': r.text,
}
],
};
final encoded = const JsonEncoder.withIndent(' ').convert(report);
_log('REPORT $encoded');
try {
final file = File('/data/data/com.example.words999.words999/cache/oracle_bakeoff.json');
await file.parent.create(recursive: true);
await file.writeAsString(encoded);
_log('WROTE ${file.path}');
} catch (e) {
_log('WRITE_FAIL $e');
}
}
void _log(String line) {
debugPrint('ORACLE_BAKEOFF|$line');
}
@override
Widget build(BuildContext context) {
final okCount = _rows.where((r) => r.ok == true).length;
return Scaffold(
backgroundColor: const Color(0xFF0C0E1A),
appBar: AppBar(
title: const Text('Сравнение провайдеров'),
actions: [
if (!_running)
IconButton(
onPressed: _run,
icon: const Icon(Icons.refresh),
tooltip: 'Запустить снова',
),
],
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
children: [
_headerCard(okCount),
const SizedBox(height: 12),
for (final row in _rows) ...[
_resultCard(row),
const SizedBox(height: 10),
],
],
),
);
}
Widget _headerCard(int okCount) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.25)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_running
? 'Идёт опрос на Android…'
: _done
? 'Готово: $okCount из ${_rows.length} ответили'
: 'Ожидание запуска',
style: const TextStyle(
color: Color(0xFFFFD54F),
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Вопрос: «$_question»',
style: const TextStyle(color: Color(0xFFE8EAF6), height: 1.35),
),
const SizedBox(height: 8),
Text(
'Знаки: ${_words.map((w) => '«${w.word.toUpperCase()}» (#${w.formattedSessionNumber})').join(' → ')}',
style: const TextStyle(color: Color(0xFF80DEEA), height: 1.35),
),
const SizedBox(height: 6),
const Text(
'Тот же system-промпт и user-промпт, что у interpretSession. Каждый провайдер вызывается отдельно, без запасного переключения.',
style: TextStyle(color: Color(0xFF8C93B8), fontSize: 12, height: 1.35),
),
if (_running)
const Padding(
padding: EdgeInsets.only(top: 12),
child: LinearProgressIndicator(color: Color(0xFFFFD54F)),
),
],
),
);
}
Widget _resultCard(ProviderProbeRow row) {
final Color accent;
if (row.running) {
accent = const Color(0xFF80DEEA);
} else if (row.ok == true) {
accent = const Color(0xFF81C784);
} else if (row.ok == false) {
accent = const Color(0xFFE57373);
} else {
accent = const Color(0xFF5C638A);
}
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF151829),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: accent.withValues(alpha: 0.45)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
row.running
? Icons.hourglass_top
: row.ok == true
? Icons.check_circle
: row.ok == false
? Icons.cancel
: Icons.circle_outlined,
color: accent,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
row.name,
style: const TextStyle(
color: Color(0xFFFFF9C4),
fontWeight: FontWeight.w700,
),
),
),
Text(
row.isWebView ? 'WebView' : 'REST',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
),
if (row.elapsedMs > 0) ...[
const SizedBox(width: 8),
Text(
'${(row.elapsedMs / 1000).toStringAsFixed(1)}с',
style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11),
),
],
],
),
const SizedBox(height: 8),
if (row.running)
const Text('Ждём полный ответ…', style: TextStyle(color: Color(0xFF80DEEA)))
else if (row.ok == true)
SelectableText(
row.text,
style: const TextStyle(color: Color(0xFFE8EAF6), height: 1.4, fontSize: 14),
)
else if (row.ok == false)
Text(
row.error ?? 'ошибка',
style: const TextStyle(color: Color(0xFFE57373), fontSize: 12, height: 1.35),
)
else
const Text('в очереди', style: TextStyle(color: Color(0xFF5C638A), fontSize: 12)),
],
),
);
}
}
+86 -89
View File
@@ -1,13 +1,32 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/history_entry_model.dart';
import '../models/word_model.dart';
import '../g4f/g4f.dart';
class AiOracleService {
static final AiOracleService _instance = AiOracleService._internal();
factory AiOracleService() => _instance;
AiOracleService._internal();
final _client = G4FClient();
static const oracleSystemPrompt =
'''Ты — Великий Оракул 999, древний архетипический разум.
Твоя задача — толковать знаки судьбы (жребий). Твой стиль речи: глубокий, философский, лаконичный, без банальностей и воды, словно голос самой вечности.
Ты не используешь современных или бытовых слов. Твои ответы строятся на символизме, психологии и скрытых смыслах. Ты не гадаешь на будущее, ты показываешь суть настоящего момента и внутренние силы человека.''';
List<Message> buildInterpretationMessages({
required List<SessionWord> words,
required String question,
}) {
return [
const Message(role: Role.system, content: oracleSystemPrompt),
Message(
role: Role.user,
content: _buildMultiWordPrompt(question: question, words: words),
),
];
}
/// Interprets a full multi-word session narrative (1 to 9 words)
Future<String> interpretSession({
required List<SessionWord> words,
@@ -15,20 +34,20 @@ class AiOracleService {
}) async {
if (words.isEmpty) return 'Нет знаков для трактовки.';
// 1. Try Live Online Endpoints (Fast timeout)
try {
final prompt = _buildMultiWordPrompt(
question: question,
words: words,
final g4fResult = await _client.chat.completions.create(
model: G4FModels.gpt4o,
messages: buildInterpretationMessages(words: words, question: question),
);
final onlineResult = await _tryFetchOnlineAi(prompt);
if (onlineResult != null && onlineResult.trim().length > 30) {
return onlineResult.trim();
if (g4fResult.trim().length > 20) {
return g4fResult.trim();
}
} catch (_) {}
} catch (e) {
print('G4F Error: $e');
}
// 2. High-Dimensional Deep Semantic Synthesizer (Instant & Deep)
// High-Dimensional Deep Semantic Synthesizer (Instant offline fallback)
return _synthesizeDeepSacredNarrative(words, question);
}
@@ -44,147 +63,125 @@ class AiOracleService {
wordsNarrative.writeln('${i + 1}. #${w.formattedSessionNumber} «${w.word.toUpperCase()}» (${w.category}): ${w.symbolicMeaning}');
}
final prompt = '''Ты — Сакральный Оракул 999. Ответь на вопрос вопрошающего: «$userMessage»
Контекст:
Вопрос: «${session.question ?? 'Поиск пути'}»
Выпавшие знаки:
final prompt = '''Вопрошающий говорит тебе: «$userMessage»
Контекст текущего сеанса судьбы:
Вопрос вопрошающего: «${session.question ?? 'Вектор судьбы (нет вопроса)'}»
Базовые знаки, которые выпали ранее:
$wordsNarrative
${newlyAddedWord != null ? "Дополнительный знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})" : ""}
Дай мудрый, глубокий и прямой ответ (2-4 предложения).''';
${newlyAddedWord != null ? "Важно! Только что вопрошающий вытянул ДОПОЛНИТЕЛЬНЫЙ уточняющий знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})." : ""}
Ответь мудро, лаконично и метафорично от лица Оракула 999. Укажи, на что направляет его судьба. (не более 3-4 предложений).''';
final messages = [
const Message(
role: Role.system,
content: 'Ты — Великий Оракул 999. Твои ответы кратки, полны символизма и психологической глубины. Ты общаешься с человеком, который ищет ответов в знаках.'
),
];
// Pass previous session history context to the model
for (final m in session.messages) {
if (m.sender == 'oracle') {
messages.add(Message(role: Role.assistant, content: m.text));
} else {
messages.add(Message(role: Role.user, content: m.text));
}
}
messages.add(Message(role: Role.user, content: prompt));
try {
final onlineResult = await _tryFetchOnlineAi(prompt);
if (onlineResult != null && onlineResult.trim().length > 20) {
return onlineResult.trim();
final g4fResult = await _client.chat.completions.create(
model: G4FModels.gpt4o,
messages: messages,
);
if (g4fResult.trim().length > 15) {
return g4fResult.trim();
}
} catch (_) {}
return _synthesizeChatReply(session, userMessage, newlyAddedWord);
}
Future<String?> _tryFetchOnlineAi(String prompt) async {
const workingModels = [
'gpt-4o-mini',
'gpt-4o',
'command-r',
'command-r7b',
'Yqcloud',
'HuggingSpace',
'WeWordle',
];
final endpoints = [
'http://localhost:8080/v1/chat/completions',
'http://127.0.0.1:8080/v1/chat/completions',
];
for (final ep in endpoints) {
for (final modelName in workingModels) {
try {
final res = await http.post(
Uri.parse(ep),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'model': modelName,
'messages': [{'role': 'user', 'content': prompt}],
'temperature': 0.7,
}),
).timeout(const Duration(seconds: 10));
if (res.statusCode == 200) {
final data = jsonDecode(utf8.decode(res.bodyBytes));
final content = data['choices']?[0]?['message']?['content'] as String?;
if (content != null && content.trim().length > 15) {
return content.trim();
}
}
} catch (_) {}
}
}
return null;
}
String _buildMultiWordPrompt({
required String question,
required List<SessionWord> words,
}) {
final qStr = question.trim().isNotEmpty ? 'Вопрос: «$question»' : 'Общий расклад судьбы';
final qStr = question.trim().isNotEmpty
? 'Вопрос, с которым пришел вопрошающий: «$question»'
: 'Вопрошающий пришел без конкретного вопроса. Прочитай его общую судьбоносную линию.';
final wordsStr = words
.asMap()
.entries
.map((e) => '${e.key + 1}. #${e.value.formattedSessionNumber} «${e.value.word.toUpperCase()}» (${e.value.category}) — ${e.value.symbolicMeaning}')
.map((e) => 'Знак ${e.key + 1}: #${e.value.formattedSessionNumber} «${e.value.word.toUpperCase()}» (${e.value.category}). Смысл знака: ${e.value.symbolicMeaning}')
.join('\n');
return '''Ты — Священный Оракул 999. Составь цельное, глубокое, психологически точное толкование выпавших знаков жребия.
$qStr
return '''$qStr
Выпавшие знаки:
Волею судьбы выпали следующие знаки жребия:
$wordsStr
Требования:
- Свяжи выпавшие знаки в единую осмысленную историю-трансформацию.
- Укажи скрытую причину, ключевой вызов и вектор исхода.
- Ответ на русском языке (3-5 предложений), образный, сильный, без шаблонных фраз.''';
Задача Оракула:
1. Свяжи эти знаки в единую цельную историю развития ситуации. Каждый знак перетекает в следующий.
2. Раскрой не очевидный, а скрытый внутренний урок. Что вопрошающий должен осознать?
3. Дай философский совет или вектор для действия.
Ответь от лица Оракула 999 на русском языке (от 3 до 6 предложений). Избегай шаблонов и клише, говори образно и метафорично, но точно.''';
}
/// High-dimensional Deep Semantic Synthesizer
String _synthesizeDeepSacredNarrative(List<SessionWord> words, String question) {
final sb = StringBuffer();
final first = words.first;
final last = words.last;
final hasQuestion = question.trim().isNotEmpty;
// Introduction / Framing
if (hasQuestion) {
sb.write('Относительно вашего вопроса «${question.trim()}» ');
} else {
sb.write('В потоке открывшегося жребия ');
sb.write('В открывшемся жребии судьбы ');
}
if (words.length == 1) {
sb.write('выпал единственный доминирующий знак — #${first.formattedSessionNumber} «${first.word.toUpperCase()}» (${first.category}). ');
sb.write('проявился доминирующий знак #${first.formattedSessionNumber} «${first.word.toUpperCase()}» (${first.category}). ');
sb.write('${first.symbolicMeaning} ');
sb.write('Этот символ требует предельной концентрации: обратите внимание на скрытые мотивы и не рассеивайте внимание на второстепенное.');
sb.write('Знак указывает на необходимость предельной ясности: держите фокус на главном и отсекайте всё наносное.');
return sb.toString();
}
if (words.length == 2) {
final second = words[1];
sb.write('проявляется взаимодействие двух сил: начального импульса #${first.formattedSessionNumber} «${first.word.toUpperCase()}» и вектора #${second.formattedSessionNumber} «${second.word.toUpperCase()}». ');
sb.write('проявляется полярность двух сил: исходного импульса #${first.formattedSessionNumber} «${first.word.toUpperCase()}» и вектора #${second.formattedSessionNumber} «${second.word.toUpperCase()}». ');
sb.write('С одной стороны, ${first.symbolicMeaning.toLowerCase()} С другой — ${second.symbolicMeaning.toLowerCase()} ');
sb.write('Разрешение ситуации лежит в точке баланса между ними: не отвергайте первое, но направляйте его энергию в созидательное русло второго.');
sb.write('Разрешение ситуации лежит в точке осознанного баланса между ними: не противьтесь первому, но направляйте силу в русло второго.');
return sb.toString();
}
// 3 or more words
sb.write('разворачивается цепь из ${words.length} взаимосвязанных знаков. ');
sb.write('разворачивается цепь из ${words.length} взаимосвязанных архетипов. ');
sb.write('Исток ситуации заложен в знаке #${first.formattedSessionNumber} «${first.word.toUpperCase()}» — ${first.symbolicMeaning.toLowerCase()} ');
// Middle transitions
if (words.length > 2) {
final middleWords = words.sublist(1, words.length - 1);
final midNames = middleWords.map((w) => '«${w.word.toUpperCase()}» (#${w.formattedSessionNumber})').join(' и ');
final midMeanings = middleWords.map((w) => w.symbolicMeaning).join(' ');
sb.write('Процесс трансформации проходит через испытание $midNames, где $midMeanings ');
sb.write('Фаза перехода и испытаний проходит через $midNames, где $midMeanings ');
}
// Resolution / Outcome
sb.write('Кульминация и ключ к исходу запечатаны в знаке #${last.formattedSessionNumber} «${last.word.toUpperCase()}» — ${last.symbolicMeaning.toLowerCase()} ');
sb.write('Оракул указывает: путь открыт через осознание скрытых уроков и решительный шаг вперед.');
sb.write('Кульминация и запечатывание исхода лежат в знаке #${last.formattedSessionNumber} «${last.word.toUpperCase()}» — ${last.symbolicMeaning.toLowerCase()} ');
sb.write('Оракул возвещает: путь открыт через принятие скрытых уроков и решительное действие.');
return sb.toString();
}
String _synthesizeChatReply(OracleSession session, String userMessage, SessionWord? newlyAddedWord) {
if (newlyAddedWord != null) {
return 'Дополнительный знак #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.category}) раскрывает скрытую грань ситуации: ${newlyAddedWord.symbolicMeaning} В контексте ваших предыдущих знаков это означает, что нужно сместить фокус внимания на внутреннюю суть происходящего.';
return 'Дополнительный знак #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.category}) проясняет скрытую деталь: ${newlyAddedWord.symbolicMeaning} В контексте общего расклада этот знак призывает обратить внимание на внутренние причины, а не внешние проявления.';
}
final wordsStr = session.words.map((w) => '«${w.word.toUpperCase()}»').join(', ');
return 'Вглядываясь в знаки ($wordsStr), Оракул отвечает на ваш вопрос: суть ответа лежит в осознании скрытых взаимосвязей. Отпустите страх неопределенности и доверьтесь логике происходящих изменений.';
return 'Вглядываясь в совокупность знаков ($wordsStr), Оракул отвечает: истинный ответ кроется в понимании скрытой связи между причиной и исходом. Доверьтесь внутреннему знанию и делайте шаг вперед.';
}
/// Legacy single word helper
Future<String> interpret({
required WordItem word,
required String question,
+1
View File
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)
@@ -5,8 +5,10 @@
import FlutterMacOS
import Foundation
import flutter_inappwebview_macos
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
+128
View File
@@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -41,6 +49,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cronet_http:
dependency: "direct main"
description:
name: cronet_http
sha256: "1b99ad5ae81aa9d2f12900e5f17d3681f3828629bb7f7fe7ad88076a34209840"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@@ -73,11 +97,83 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
flutter_inappwebview_android:
dependency: transitive
description:
name: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
flutter_inappwebview_internal_annotations:
dependency: transitive
description:
name: flutter_inappwebview_internal_annotations
sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6
url: "https://pub.dev"
source: hosted
version: "1.3.0"
flutter_inappwebview_ios:
dependency: transitive
description:
name: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_macos:
dependency: transitive
description:
name: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_platform_interface:
dependency: transitive
description:
name: flutter_inappwebview_platform_interface
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
url: "https://pub.dev"
source: hosted
version: "1.3.0+1"
flutter_inappwebview_web:
dependency: transitive
description:
name: flutter_inappwebview_web
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_inappwebview_windows:
dependency: transitive
description:
name: flutter_inappwebview_windows
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -112,6 +208,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
http_profile:
dependency: transitive
description:
name: http_profile
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
jni:
dependency: transitive
description:
name: jni
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
url: "https://pub.dev"
source: hosted
version: "0.14.2"
leak_tracker:
dependency: transitive
description:
@@ -168,6 +280,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
@@ -333,6 +453,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
vector_math:
dependency: transitive
description:
+4 -1
View File
@@ -36,7 +36,10 @@ dependencies:
cupertino_icons: ^1.0.8
shared_preferences: ^2.5.5
http: ^1.6.0
cronet_http: 1.5.0
crypto: ^3.0.7
flutter_inappwebview: ^6.1.5
uuid: ^4.6.0
dev_dependencies:
flutter_test:
sdk: flutter
+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);
});
}
@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
}
+2
View File
@@ -3,9 +3,11 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_inappwebview_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)