import { BaseHandler } from './BaseHandler'; import { IPromptOptions, IPromptGenericConfig, ModuleOptions } from '../../types'; import { libx } from 'libx.js/build/bundles/essentials.js'; /** * CloudflareHandler - For Cloudflare Workers AI models * Uses Cloudflare's Workers AI API (when running on Workers) * Or uses the REST API (when running elsewhere) */ export class CloudflareHandler extends BaseHandler { private baseUrl = 'https://api.cloudflare.com/client/v4/accounts'; private accountId?: string; constructor(options: ModuleOptions) { super(options); // Account ID should be provided via options or env this.accountId = options.cloudflareAccountId; } async execute(isStream: boolean, options: IPromptOptions): Promise { const isPlain = options.config.plain ?? false; try { const config = this.getConfig(options.config); const apiKey = this.getApiKey('cloudflare'); if (!apiKey) { throw new Error('Cloudflare API key not configured'); } if (!this.accountId) { throw new Error('Cloudflare Account ID not configured'); } const messages = this.prepareMessages(options); // Cloudflare Workers AI uses a simplified payload const payload: any = { messages, }; // Add optional parameters if supported by model if (config.temperature !== undefined) payload.temperature = config.temperature; if (config.max_tokens !== undefined) payload.max_tokens = config.max_tokens; if (config.top_p !== undefined) payload.top_p = config.top_p; // Stream parameter payload.stream = isStream; if (isStream) { return await this.executeStream(payload, apiKey, config.model, isPlain); } else { return await this.executeSync(payload, apiKey, config.model, isPlain); } } catch (error) { return this.handleError(error, isPlain); } } getConfig(generalConfig: IPromptGenericConfig): any { let model = generalConfig.model || '@cf/meta/llama-2-7b-chat-fp16'; // Strip provider prefix if present model = this.normalizeModelName(model, 'cloudflare'); // Ensure model has the @ prefix for Cloudflare models if (!model.startsWith('@')) { model = `@${model}`; } const maxTokens = generalConfig.maxTokens || 2000; return { model, temperature: generalConfig.temperature ?? 0.7, max_tokens: maxTokens, top_p: generalConfig.topP, }; } private prepareMessages(options: IPromptOptions): any[] { const messages: any[] = []; if (options.systemPrompt) { messages.push({ role: 'system', content: options.systemPrompt }); } if (options.messages) { for (const msg of options.messages) { messages.push({ role: msg.role, content: msg.content, }); } } return messages; } private async executeStream(payload: any, apiKey: string, model: string, isPlain: boolean): Promise { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); const encoder = new TextEncoder(); const url = `${this.baseUrl}/${this.accountId}/ai/run/${model}`; libx.log.v('CloudflareHandler: streaming request to', url); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(payload) }); if (!response.ok) { const error = await response.text(); throw new Error(`Cloudflare API error: ${error}`); } if (!response.body) { throw new Error('No response body'); } // Process the streaming response const reader = response.body.getReader(); const decoder = new TextDecoder(); const processStream = async () => { try { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.slice(6); if (jsonStr === '[DONE]') continue; try { const data = JSON.parse(jsonStr); const delta = data.response || data.content || data.delta?.content; if (delta) { await this.writeStreamChunk(writer, encoder, delta, isPlain); } } catch (e) { libx.log.w('CloudflareHandler: failed to parse chunk', e); } } } } if (!isPlain) { writer.write(encoder.encode('data: [DONE]\n\n')); } writer.close(); } catch (error) { libx.log.e('CloudflareHandler: stream error', error); if (isPlain) { writer.write(encoder.encode(`\nError: ${(error as Error).message}`)); } else { writer.write(encoder.encode(`data: ${JSON.stringify({ error: (error as Error).message })}\n\n`)); } writer.close(); } }; processStream(); } catch (error) { libx.log.e('CloudflareHandler: request error', error); if (isPlain) { writer.write(encoder.encode(`Error: ${(error as Error).message}`)); } else { writer.write(encoder.encode(`data: ${JSON.stringify({ error: (error as Error).message })}\n\n`)); } writer.close(); } return readable; } private async executeSync(payload: any, apiKey: string, model: string, isPlain: boolean): Promise { const url = `${this.baseUrl}/${this.accountId}/ai/run/${model}`; libx.log.v('CloudflareHandler: request to', url); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(payload) }); if (!response.ok) { const error = await response.text(); throw new Error(`Cloudflare API error: ${error}`); } const data = await response.json(); const content = data.result?.response || data.result?.content || ''; const metadata = { model, success: data.success }; return this.writeSyncResponse(content, metadata, isPlain); } }