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.
76 lines
2.5 KiB
Dart
76 lines
2.5 KiB
Dart
const kBrowserUserAgent =
|
|
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36';
|
|
|
|
const kDesktopUserAgent =
|
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
|
|
Map<String, String> browserHeaders({
|
|
String? origin,
|
|
String? referer,
|
|
String? accept,
|
|
String? contentType,
|
|
bool desktop = false,
|
|
Map<String, String>? extra,
|
|
}) {
|
|
return {
|
|
'User-Agent': desktop ? kDesktopUserAgent : kBrowserUserAgent,
|
|
'Accept': accept ?? '*/*',
|
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
if (origin != null) 'Origin': origin,
|
|
if (referer != null) 'Referer': referer,
|
|
if (contentType != null) 'Content-Type': contentType,
|
|
...?extra,
|
|
};
|
|
}
|
|
|
|
/// True when a provider dumped an error page / WAF / paywall instead of model text.
|
|
bool looksLikeGarbage(String text) {
|
|
final t = text.trimLeft();
|
|
if (t.isEmpty) return true;
|
|
final lower = t.toLowerCase();
|
|
if (t.startsWith('<!DOCTYPE') || t.startsWith('<html') || t.startsWith('<HTML')) {
|
|
return true;
|
|
}
|
|
if (lower.contains('just a moment') ||
|
|
lower.contains('cf-browser-verification') ||
|
|
lower.contains('attention required') ||
|
|
lower.contains('payment required') ||
|
|
lower.contains('frontend_captcha') ||
|
|
lower.contains('err_challenge') ||
|
|
lower.contains('error code:') ||
|
|
lower.contains('missing api key') ||
|
|
lower.contains('invalid api key') ||
|
|
lower.contains('you have to be logged in') ||
|
|
lower.contains('no cake credits') ||
|
|
lower.contains('insufficient_credits')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool looksLikeModelOutput(String text) {
|
|
final trimmed = text.trim();
|
|
if (trimmed.length < 8) return false;
|
|
final lower = trimmed.toLowerCase();
|
|
if (lower == ': ping' || lower == 'ping' || lower == 'pong' || lower == '{}') {
|
|
return false;
|
|
}
|
|
if (RegExp(r'^[\{\}\[\]":\s]+$').hasMatch(trimmed)) return false;
|
|
return !looksLikeGarbage(trimmed);
|
|
}
|
|
|
|
String? cookieHeaderFromSetCookie(String? setCookie, {String? name}) {
|
|
if (setCookie == null || setCookie.isEmpty) return null;
|
|
// http package may join multiple Set-Cookie values with ", ".
|
|
final parts = setCookie.split(RegExp(r',(?=\s*[^;=]+=)'));
|
|
final pairs = <String>[];
|
|
for (final part in parts) {
|
|
final nv = part.split(';').first.trim();
|
|
if (nv.isEmpty || !nv.contains('=')) continue;
|
|
if (name != null && !nv.startsWith('$name=')) continue;
|
|
pairs.add(nv);
|
|
}
|
|
if (pairs.isEmpty) return null;
|
|
return pairs.join('; ');
|
|
}
|