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