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
+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;
}
}