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 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 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; } }