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:
@@ -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(' | ')}');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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))}');
|
||||
}
|
||||
}
|
||||
@@ -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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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))}');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user