fbf04d90db
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.
104 lines
3.1 KiB
Dart
104 lines
3.1 KiB
Dart
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,
|
|
});
|
|
}
|