import { Router } from "express"; import { getSession, addMessage, getConversationForAPI } from "../services/sessionStore.js"; import { trackUsage, getUsageSummary, formatCost, getSessionUsage } from "../services/usageTracker.js"; import { getCloudflareAccountId, getCloudflareApiToken } from "../services/providerConfig.js"; import { claudeToolsToCloudflare, claudeMessagesToCloudflare, cloudflareResponseToClaude } from "../services/toolConverter.js"; const router = Router(); /** * Build entity context string for the system prompt */ function buildEntityContext(entities) { if (!entities || entities.length === 0) { return "\n\nThe diagram is currently empty."; } const entityList = entities.map(e => { const shape = e.template?.replace('#', '').replace('-template', '') || 'unknown'; const pos = e.position || { x: 0, y: 0, z: 0 }; return `- ${e.text || '(no label)'} (${shape}, ${e.color || 'unknown'}) at (${pos.x?.toFixed(1)}, ${pos.y?.toFixed(1)}, ${pos.z?.toFixed(1)})`; }).join('\n'); return `\n\n## Current Diagram State\nThe diagram currently contains ${entities.length} entities:\n${entityList}`; } // Express 5 uses named parameters for wildcards router.post("/*path", async (req, res) => { const requestStart = Date.now(); console.log(`[Cloudflare API] ========== REQUEST START ==========`); const accountId = getCloudflareAccountId(); const apiToken = getCloudflareApiToken(); if (!accountId) { console.error(`[Cloudflare API] ERROR: Account ID not configured`); return res.status(500).json({ error: "Cloudflare account ID not configured" }); } if (!apiToken) { console.error(`[Cloudflare API] ERROR: API token not configured`); return res.status(500).json({ error: "Cloudflare API token not configured" }); } // Check for session-based request const { sessionId, ...requestBody } = req.body; let modifiedBody = { ...requestBody }; const model = requestBody.model; console.log(`[Cloudflare API] Session ID: ${sessionId || 'none'}`); console.log(`[Cloudflare API] Model: ${model}`); console.log(`[Cloudflare API] Messages count: ${requestBody.messages?.length || 0}`); // Build system prompt with entity context let systemPrompt = modifiedBody.system || ''; if (sessionId) { const session = getSession(sessionId); if (session) { console.log(`[Cloudflare API] Session found: ${session.entities.length} entities, ${session.conversationHistory.length} messages in history`); // Inject entity context into system prompt const entityContext = buildEntityContext(session.entities); console.log(`[Cloudflare API] Entity context added (${entityContext.length} chars)`); systemPrompt += entityContext; // Get conversation history and merge with current messages const historyMessages = getConversationForAPI(sessionId); if (historyMessages.length > 0 && modifiedBody.messages) { const currentContent = modifiedBody.messages[modifiedBody.messages.length - 1]?.content; const filteredHistory = historyMessages.filter(msg => msg.content !== currentContent); modifiedBody.messages = [...filteredHistory, ...modifiedBody.messages]; console.log(`[Cloudflare API] Merged ${filteredHistory.length} history + ${modifiedBody.messages.length - filteredHistory.length} new = ${modifiedBody.messages.length} total messages`); } } else { console.log(`[Cloudflare API] WARNING: Session ${sessionId} not found`); } } try { // Convert to Cloudflare format const cfMessages = claudeMessagesToCloudflare(modifiedBody.messages || [], systemPrompt); const cfTools = modifiedBody.tools ? claudeToolsToCloudflare(modifiedBody.tools) : undefined; // Build Cloudflare request body const cfRequestBody = { messages: cfMessages, max_tokens: modifiedBody.max_tokens || 1024 }; // Only include tools if the model supports them if (cfTools && cfTools.length > 0) { cfRequestBody.tools = cfTools; } // Cloudflare endpoint: https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model} const endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`; console.log(`[Cloudflare API] Sending request to: ${endpoint}`); console.log(`[Cloudflare API] Request body messages: ${cfMessages.length}, tools: ${cfTools?.length || 0}`); const requestBodyJson = JSON.stringify(cfRequestBody); console.log(`[Cloudflare API] Full request body (${requestBodyJson.length} bytes):`); console.log(requestBodyJson); const fetchStart = Date.now(); const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}`, }, body: JSON.stringify(cfRequestBody), }); const fetchDuration = Date.now() - fetchStart; console.log(`[Cloudflare API] Response received in ${fetchDuration}ms, status: ${response.status}`); console.log(`[Cloudflare API] Parsing response JSON...`); const cfData = await response.json(); if (!cfData.success) { console.error(`[Cloudflare API] API returned error:`, cfData.errors); return res.status(response.status).json({ error: cfData.errors?.[0]?.message || "Cloudflare API error", details: cfData.errors }); } // Convert Cloudflare response to Claude format const data = cloudflareResponseToClaude(cfData, model); console.log(`[Cloudflare API] Response converted. Stop reason: ${data.stop_reason}, content blocks: ${data.content?.length || 0}`); // Track and log token usage if (data.usage) { // Extract content for detailed tracking const userMessage = requestBody.messages?.[requestBody.messages.length - 1]; const inputText = typeof userMessage?.content === 'string' ? userMessage.content : null; const outputText = data.content ?.filter(c => c.type === 'text') .map(c => c.text) .join('\n') || null; const toolCalls = data.content ?.filter(c => c.type === 'tool_use') .map(c => ({ name: c.name, input: c.input })) || []; const usageRecord = trackUsage(sessionId, model, data.usage, { inputText, outputText, toolCalls }); console.log(`[Cloudflare API] REQUEST USAGE: ${getUsageSummary(usageRecord)}`); // Log cumulative session usage if session exists if (sessionId) { const sessionStats = getSessionUsage(sessionId); if (sessionStats) { console.log(`[Cloudflare API] SESSION TOTALS (${sessionStats.requestCount} requests):`); console.log(`[Cloudflare API] Total input: ${sessionStats.totalInputTokens} tokens`); console.log(`[Cloudflare API] Total output: ${sessionStats.totalOutputTokens} tokens`); console.log(`[Cloudflare API] Total cost: ${formatCost(sessionStats.totalCost)}`); } } } // If session exists and response is successful, store messages if (sessionId && response.ok && data.content) { const session = getSession(sessionId); if (session) { // Store the user message if it was new (only if it's a string, not tool results) const userMessage = requestBody.messages?.[requestBody.messages.length - 1]; if (userMessage && userMessage.role === 'user' && typeof userMessage.content === 'string') { addMessage(sessionId, { role: 'user', content: userMessage.content }); console.log(`[Cloudflare API] Stored user message to session`); } // Store the assistant response (text only, not tool use blocks) const assistantContent = data.content .filter(c => c.type === 'text') .map(c => c.text) .join('\n'); if (assistantContent) { addMessage(sessionId, { role: 'assistant', content: assistantContent }); console.log(`[Cloudflare API] Stored assistant response to session (${assistantContent.length} chars)`); } } } const totalDuration = Date.now() - requestStart; console.log(`[Cloudflare API] ========== REQUEST COMPLETE (${totalDuration}ms) ==========`); res.status(response.status).json(data); } catch (error) { const totalDuration = Date.now() - requestStart; console.error(`[Cloudflare API] ========== REQUEST FAILED (${totalDuration}ms) ==========`); console.error(`[Cloudflare API] Error:`, error); console.error(`[Cloudflare API] Error message:`, error.message); console.error(`[Cloudflare API] Error stack:`, error.stack); res.status(500).json({ error: "Failed to proxy request to Cloudflare API", details: error.message }); } }); export default router;