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