import { getModelById, getVideoModelById, getI2IModelById, getI2VModelById, getV2VModelById, getLipSyncModelById } from './models.js'; const BASE_URL = 'https://api.muapi.ai'; const PROXY_WF_BASE = '/api/workflow'; async function pollForResult(requestId, key, maxAttempts = 900, interval = 2000) { const pollUrl = `${BASE_URL}/api/v1/predictions/${requestId}/result`; for (let attempt = 1; attempt <= maxAttempts; attempt++) { await new Promise(resolve => setTimeout(resolve, interval)); try { const response = await fetch(pollUrl, { headers: { 'Content-Type': 'application/json', 'x-api-key': key } }); if (!response.ok) { const errText = await response.text(); if (response.status >= 500) continue; throw new Error(`Poll Failed: ${response.status} - ${errText.slice(0, 100)}`); } const data = await response.json(); const status = data.status?.toLowerCase(); if (status === 'completed' || status === 'succeeded' || status === 'success') return data; if (status === 'failed' || status === 'error') throw new Error(`Generation failed: ${data.error || 'Unknown error'}`); } catch (error) { if (attempt === maxAttempts) throw error; } } throw new Error('Generation timed out after polling.'); } async function submitAndPoll(endpoint, payload, key, onRequestId, maxAttempts = 60) { const url = `${BASE_URL}/api/v1/${endpoint}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': key }, body: JSON.stringify(payload) }); if (!response.ok) { const errText = await response.text(); throw new Error(`API Request Failed: ${response.status} ${response.statusText} - ${errText.slice(0, 100)}`); } const submitData = await response.json(); const requestId = submitData.request_id || submitData.id; if (!requestId) return submitData; if (onRequestId) onRequestId(requestId); const result = await pollForResult(requestId, key, maxAttempts); const outputUrl = result.outputs?.[0] || result.url || result.output?.url; return { ...result, url: outputUrl }; } export async function generateImage(apiKey, params) { const modelInfo = getModelById(params.model); const endpoint = modelInfo?.endpoint || params.model; const payload = { prompt: params.prompt }; if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio; if (params.resolution) payload.resolution = params.resolution; if (params.quality) payload.quality = params.quality; if (params.image_url) { payload.image_url = params.image_url; payload.strength = params.strength || 0.6; } else payload.image_url = null; if (params.seed && params.seed !== -1) payload.seed = params.seed; return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 60); } export async function generateI2I(apiKey, params) { const modelInfo = getI2IModelById(params.model); const endpoint = modelInfo?.endpoint || params.model; const payload = {}; if (params.prompt) payload.prompt = params.prompt; const imageField = modelInfo?.imageField || 'image_url'; const imagesList = params.images_list?.length > 0 ? params.images_list : (params.image_url ? [params.image_url] : null); if (imagesList) { if (imageField === 'images_list') payload.images_list = imagesList; else payload[imageField] = imagesList[0]; } if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio; if (params.resolution) payload.resolution = params.resolution; if (params.quality) payload.quality = params.quality; return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 60); } export async function generateVideo(apiKey, params) { const modelInfo = getVideoModelById(params.model); const endpoint = modelInfo?.endpoint || params.model; const payload = {}; if (params.prompt) payload.prompt = params.prompt; if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio; if (params.duration) payload.duration = params.duration; if (params.resolution) payload.resolution = params.resolution; if (params.quality) payload.quality = params.quality; if (params.mode) payload.mode = params.mode; if (params.image_url) payload.image_url = params.image_url; return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 900); } export async function generateI2V(apiKey, params) { const modelInfo = getI2VModelById(params.model); const endpoint = modelInfo?.endpoint || params.model; const payload = {}; if (params.prompt) payload.prompt = params.prompt; const imageField = modelInfo?.imageField || 'image_url'; if (params.image_url) { if (imageField === 'images_list') payload.images_list = [params.image_url]; else payload[imageField] = params.image_url; } if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio; if (params.duration) payload.duration = params.duration; if (params.resolution) payload.resolution = params.resolution; if (params.quality) payload.quality = params.quality; if (params.mode) payload.mode = params.mode; return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 900); } export async function processLipSync(apiKey, params) { const modelInfo = getLipSyncModelById(params.model); const endpoint = modelInfo?.endpoint || params.model; const payload = {}; if (params.audio_url) payload.audio_url = params.audio_url; if (params.image_url) payload.image_url = params.image_url; if (params.video_url) payload.video_url = params.video_url; if (params.prompt) payload.prompt = params.prompt; if (params.resolution) payload.resolution = params.resolution; if (params.seed !== undefined && params.seed !== -1) payload.seed = params.seed; return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 900); } export function uploadFile(apiKey, file, onProgress) { return new Promise((resolve, reject) => { const url = `${BASE_URL}/api/v1/upload_file`; const formData = new FormData(); formData.append('file', file); const xhr = new XMLHttpRequest(); xhr.open('POST', url); xhr.setRequestHeader('x-api-key', apiKey); if (onProgress) { xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percentComplete = Math.round((event.loaded / event.total) * 100); onProgress(percentComplete); } }; } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const data = JSON.parse(xhr.responseText); const fileUrl = data.url || data.file_url || data.data?.url; if (!fileUrl) { reject(new Error('No URL returned from file upload')); } else { resolve(fileUrl); } } catch (e) { reject(new Error('Failed to parse upload response')); } } else { let detail = xhr.statusText; try { const errObj = JSON.parse(xhr.responseText); detail = errObj.detail || detail; } catch (e) { // fallback to statusText } reject(new Error(`File upload failed: ${xhr.status} - ${detail}`)); } }; xhr.onerror = () => reject(new Error('Network error during file upload')); xhr.send(formData); }); } export async function getUserBalance(apiKey) { const response = await fetch(`${BASE_URL}/api/v1/account/balance`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch balance: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); } export async function getTemplateWorkflows(apiKey) { const response = await fetch(`${BASE_URL}/workflow/get-template-workflows`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch template workflows: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function getUserWorkflows(apiKey) { const response = await fetch(`${BASE_URL}/workflow/get-workflow-defs`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch user workflows: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function getPublishedWorkflows(apiKey) { const response = await fetch(`${BASE_URL}/workflow/get-published-workflows`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch published workflows: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; // Agents — uses direct URL → https://api.muapi.ai/agents/... export async function getTemplateAgents(apiKey) { const response = await fetch(`${BASE_URL}/agents/templates/agents`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch template agents: ${response.status} - ${errText.slice(0, 100)}`); } const data = await response.json(); return Array.isArray(data) ? data : (data.agents || data.items || []); }; export async function getUserAgents(apiKey) { const response = await fetch(`${BASE_URL}/agents/user/agents`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch user agents: ${response.status} - ${errText.slice(0, 100)}`); } const data = await response.json(); return Array.isArray(data) ? data : (data.agents || data.items || []); }; export async function getPublishedAgents(apiKey) { // MuAPI: GET /agents/featured/agents const response = await fetch(`${BASE_URL}/agents/featured/agents`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch featured agents: ${response.status} - ${errText.slice(0, 100)}`); } const data = await response.json(); return Array.isArray(data) ? data : (data.agents || data.items || []); }; // GET /agents/user/conversations — returns the user's chat history across all agents export async function getUserConversations(apiKey) { const response = await fetch(`${BASE_URL}/agents/user/conversations`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch conversations: ${response.status} - ${errText.slice(0, 100)}`); } const data = await response.json(); return Array.isArray(data) ? data : []; }; export async function createWorkflow(apiKey, payload) { const response = await fetch(`${BASE_URL}/workflow/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify(payload) }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to create workflow: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function updateWorkflowName(apiKey, workflowId, name) { const response = await fetch(`${BASE_URL}/workflow/update-name/${workflowId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify({ name }) }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to rename workflow: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function deleteWorkflow(apiKey, workflowId) { const response = await fetch(`${BASE_URL}/workflow/delete-workflow-def/${workflowId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to delete workflow: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function getWorkflowInputs(apiKey, workflowId) { const response = await fetch(`${BASE_URL}/workflow/${workflowId}/api-inputs`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch workflow inputs: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function executeWorkflow(apiKey, workflowId, inputs) { const response = await fetch(`${BASE_URL}/workflow/${workflowId}/api-execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify({ inputs }) }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to execute workflow: ${response.status} - ${errText.slice(0, 100)}`); } const submitData = await response.json(); const runId = submitData.run_id || submitData.id; if (!runId) return submitData; // Poll for results return await pollWorkflowResult(runId, apiKey); }; async function pollWorkflowResult(runId, apiKey, maxAttempts = 900, interval = 2000) { const pollUrl = `${BASE_URL}/workflow/run/${runId}/api-outputs`; for (let attempt = 1; attempt <= maxAttempts; attempt++) { await new Promise(resolve => setTimeout(resolve, interval)); try { const response = await fetch(pollUrl, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { if (response.status >= 500) continue; throw new Error(`Poll Failed: ${response.status}`); } const data = await response.json(); const status = data.status?.toLowerCase(); if (status === 'completed' || status === 'succeeded' || status === 'success') return data; if (status === 'failed' || status === 'error') throw new Error(`Workflow failed: ${data.error || 'Unknown error'}`); } catch (error) { if (attempt === maxAttempts) throw error; } } throw new Error('Workflow timed out after polling.'); }; export async function getAllNodeSchemas(apiKey, workflowId) { const response = await fetch(`${BASE_URL}/workflow/${workflowId}/node-schemas`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch node schemas: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function getWorkflowData(apiKey, workflowId) { const response = await fetch(`${BASE_URL}/workflow/get-workflow-def/${workflowId}`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch workflow data: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }; export async function getNodeSchemas(apiKey, workflowId) { const response = await fetch(`${BASE_URL}/workflow/${workflowId}/api-node-schemas`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to fetch node schemas: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); } export async function runSingleNode(apiKey, workflowId, nodeId, payload) { const response = await fetch(`${BASE_URL}/workflow/${workflowId}/node/${nodeId}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify(payload) }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to run single node: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); } export async function deleteNodeRun(apiKey, nodeRunId) { const response = await fetch(`${BASE_URL}/workflow/node-run/${nodeRunId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to delete node run: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); } export async function getNodeStatus(apiKey, runId) { const response = await fetch(`${BASE_URL}/workflow/run/${runId}/status`, { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey } }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to get node status: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); } /** * Handle proxy requests centralizing communication logic with MuAPI. * This is used by the server-side entry points. */ export async function handleProxyRequest(prefix, path, method, headers, body, apiKey) { const url = `${BASE_URL}/${prefix}/${path}`; const finalHeaders = new Headers(headers); finalHeaders.delete('host'); finalHeaders.delete('connection'); finalHeaders.delete('content-length'); // Let fetch recalculate this for safety if (apiKey) { finalHeaders.set('x-api-key', apiKey); } try { const response = await fetch(url, { method, headers: finalHeaders, body: (method !== 'GET' && method !== 'HEAD') ? body : undefined, redirect: 'follow', }); const contentType = response.headers.get('Content-Type') || 'application/json'; const buffer = await response.arrayBuffer(); return { status: response.status, contentType, data: buffer }; } catch (error) { console.error(`MuAPI Proxy error for ${url}:`, error); throw error; } } /** * A centralized handler for Next.js API routes or middleware. */ export async function handleServerSideProxy(prefix, request, params, apiKey) { try { const slug = await params; const pathSegments = slug.path || []; const path = pathSegments.join('/'); const method = request.method; let body = null; if (method !== 'GET' && method !== 'HEAD') { body = await request.arrayBuffer(); } const { search } = new URL(request.url); const pathWithSearch = search ? `${path}${search}` : path; return await handleProxyRequest( prefix, pathWithSearch, method, request.headers, body, apiKey ); } catch (error) { console.error(`Server proxy failed:`, error); throw error; } } export async function calculateDynamicCost(apiKey, taskName, payload) { const response = await fetch(`${BASE_URL}/api/v1/app/calculate_dynamic_cost`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify({ task_name: taskName, payload }) }); if (!response.ok) { const errText = await response.text(); throw new Error(`Failed to calculate dynamic cost: ${response.status} - ${errText.slice(0, 100)}`); } return await response.json(); }