Node.js

Using OpenRouter

Replace this in your server file:

app.post('/openrouter', async (req, res) => {
    ...
});

with this:

/*
Marks the end of the conversation as cacheable, so the next call in the same
Code Chat request re-uses everything sent so far instead of paying for it again.
*/
function cbMarkConversationCacheable(messages) {
    const out = messages.slice();
    for (let i = out.length - 1; i >= 0; i--) {
        const m = out[i];
        if (!m || !m.role) continue;
        if (Array.isArray(m.content)) {
            const parts = m.content.map(p => ({ ...p }));
            const last = parts[parts.length - 1];
            if (last && last.type === 'text') {
                parts[parts.length - 1] = { ...last, cache_control: { type: 'ephemeral' } };
                out[i] = { ...m, content: parts };
                return out;
            }
            continue;
        }
        if (typeof m.content === 'string' && m.content.length > 0) {
            out[i] = { ...m, content: [{ type: 'text', text: m.content, cache_control: { type: 'ephemeral' } }] };
            return out;
        }
    }
    return out;
}

/*
Streams the reply back to Code Chat, so the answer appears as it is written.
Used automatically when the request has `stream: true`.
*/
async function serveConversationStream(req, res, { url, apiKey, defaultModel }) {

    const { messages, tools, system, model, temperature, topP } = req.body;

    if (!Array.isArray(messages) || messages.length === 0) {
        return res.json({ error: 'messages must be a non-empty array' });
    }

    const cacheable = system && system.length > 2000 && req.body.cacheContext !== false;
    const systemMessage = cacheable
        ? { role: 'system', content: [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }] }
        : { role: 'system', content: system };

    const streamMessages = cacheable ? cbMarkConversationCacheable(messages) : messages;

    const payload = {
        model: model || defaultModel,
        messages: system ? [systemMessage, ...streamMessages] : streamMessages,
        temperature: temperature === undefined ? 0.7 : parseFloat(temperature),
        top_p: topP === undefined ? 0.9 : parseFloat(topP),
        stream: true,
        stream_options: { include_usage: true },
    };
    if (Array.isArray(tools) && tools.length > 0) {
        payload.tools = tools.map(t => ({ type: 'function', function: t }));
        payload.tool_choice = 'auto';
    }

    const send = () => axios.post(url, payload, {
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
        responseType: 'stream',
        // An attached image makes the request bigger than axios sends by default
        maxBodyLength: Infinity,
        maxContentLength: Infinity,
    });

    let upstream;
    try {
        try {
            upstream = await send();
        } catch (first) {
            /*
            Same retry as the non-streaming path: some newer OpenAI models apply
            a default reasoning effort their chat/completions endpoint will not
            combine with function tools, and say so. Older models reject the
            argument outright, so it cannot always be sent — the provider's own
            message decides, once.

            The rejection body is a stream here, so it has to be read before it
            can be matched against.
            */
            const body = await (async () => {
                const d = first.response && first.response.data;
                if (d && typeof d.pipe === 'function') {
                    return await new Promise(resolve => {
                        let buf = '';
                        d.on('data', c => { buf += c.toString(); });
                        d.on('end', () => resolve(buf));
                        d.on('error', () => resolve(''));
                    });
                }
                return d ? (typeof d === 'string' ? d : JSON.stringify(d)) : '';
            })();

            if (/reasoning_effort/i.test(body) && payload.reasoning_effort === undefined) {
                payload.reasoning_effort = 'none';
                upstream = await send();
            } else {
                first.__body = body;   // reuse below rather than re-reading a spent stream
                throw first;
            }
        }
    } catch (e) {
        // Pass the provider's status through, so Code Chat waits and retries
        // when you are rate limited instead of giving up.
        const status = (e.response && e.response.status) || 0;
        const retryAfter = e.response && e.response.headers && e.response.headers['retry-after'];

        // Include what the provider actually said. Without it a rejected
        // request reads only as "status code 400", which says nothing about
        // which part of the request it refused.
        let detail = e.__body || '';
        try {
            const d = detail ? null : (e.response && e.response.data);
            if (d && typeof d.pipe === 'function') {
                detail = await new Promise(resolve => {
                    let buf = '';
                    d.on('data', c => { buf += c.toString(); });
                    d.on('end', () => resolve(buf));
                    d.on('error', () => resolve(''));
                });
            } else if (d) {
                detail = typeof d === 'string' ? d : JSON.stringify(d);
            }
        } catch (err) { /* keep the plain message */ }

        if (detail) console.log('[provider error]', String(detail).slice(0, 800));
        const parsed = (() => { try { return JSON.parse(detail); } catch (err) { return null; } })();
        const message = (parsed && parsed.error && parsed.error.message) || e.message;

        return res.json({ error: message, status, retryAfter: retryAfter ? Number(retryAfter) : undefined });
    }

    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    /*
    A chunk can end mid-line. Hold the remainder until the rest arrives, or the
    line is forwarded truncated and its tail is dropped — which corrupts long
    values such as the HTML in a tool call.
    */
    let pending = '';
    upstream.data.on('data', chunk => {
        pending += chunk.toString();
        const lines = pending.split('\n');
        pending = lines.pop();
        lines.forEach(line => {
            if (line.trim().startsWith('data:')) res.write(`${line}\n`);
        });
    });
    upstream.data.on('end', () => {
        if (pending.trim().startsWith('data:')) res.write(`${pending}\n`);
        res.write('data: [DONE]\n\n');
        res.end();
    });
    upstream.data.on('error', () => { try { res.end(); } catch (e) { /* already closed */ } });
}

/*
────────────────────────────────────────────────────────────────────────────
Code Chat v2  (chatEngine: 'v2')

Handles a v2 request: the conversation is passed through to your AI provider
and the model's reply is returned as-is.
────────────────────────────────────────────────────────────────────────────
*/
async function serveConversation(req, res, { url, apiKey, defaultModel }) {

    // Streaming is a flag on the same request, not a second endpoint.
    if (req.body && req.body.stream) {
        return serveConversationStream(req, res, { url, apiKey, defaultModel });
    }

    const { messages, tools, system, model, temperature, topP } = req.body;

    if (!Array.isArray(messages) || messages.length === 0) {
        return res.json({ error: 'messages must be a non-empty array' });
    }

    // Mark the instructions as cacheable. One Code Chat request makes several
    // calls with the same instructions, so this cuts the cost significantly.
    const cacheable = system && system.length > 2000 && req.body.cacheContext !== false;
    const systemMessage = cacheable
        ? { role: 'system', content: [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }] }
        : { role: 'system', content: system };

    // Do the same for a message carrying an attached image: it is large, and
    // it would otherwise be re-sent on every call.
    const conversation = messages.slice();
    if (cacheable) {
        const i = conversation.findIndex(m => m && m.role === 'user' && Array.isArray(m.content)
            && m.content.some(part => part && part.type === 'image_url'));
        if (i !== -1) {
            const parts = conversation[i].content.map(p => ({ ...p }));
            parts[parts.length - 1] = { ...parts[parts.length - 1], cache_control: { type: 'ephemeral' } };
            conversation[i] = { ...conversation[i], content: parts };
        }
    }

    // Cache what has been sent so far, so later calls in the same request do not
    // pay for the whole conversation again.
    const cached = cacheable ? cbMarkConversationCacheable(conversation) : conversation;

    const payload = {
        model: model || defaultModel,
        messages: system ? [systemMessage, ...cached] : cached,
        temperature: temperature === undefined ? 0.7 : parseFloat(temperature),
        top_p: topP === undefined ? 0.9 : parseFloat(topP),
    };

    // Code Chat sends plain tool definitions; providers expect each wrapped.
    if (Array.isArray(tools) && tools.length > 0) {
        payload.tools = tools.map(t => ({ type: 'function', function: t }));
        payload.tool_choice = 'auto';
    }

    const send = () => axios.post(url, payload, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiKey}`,
        },
        // An attached image makes the request bigger than axios sends by default
        maxBodyLength: Infinity,
        maxContentLength: Infinity,
        validateStatus: () => true,
    });

    let response = await send();

    /*
    Some newer OpenAI models apply a default reasoning effort that their
    chat/completions endpoint will not combine with function tools, and answer:
    "Function tools with reasoning_effort are not supported ... set
    reasoning_effort to 'none'". Older models reject that argument outright, so
    it cannot simply always be sent, and which models need it is not something
    to guess from a name. The provider says so plainly, so the retry is driven
    by what it said — once, and only for that message.
    */
    if (response.status >= 400
        && /reasoning_effort/i.test((response.data && response.data.error && response.data.error.message) || '')
        && payload.reasoning_effort === undefined) {
        payload.reasoning_effort = 'none';
        response = await send();
    }

    if (response.status >= 400) {
        const retryAfter = response.headers && response.headers['retry-after'];
        return res.json({
            error: (response.data && response.data.error && response.data.error.message) || `Request failed with status code ${response.status}`,
            status: response.status,
            retryAfter: retryAfter ? Number(retryAfter) : undefined,
        });
    }

    if (response.data && response.data.error) {
        return res.json({ error: response.data.error.message || String(response.data.error) });
    }

    const choice = response.data.choices && response.data.choices[0];
    if (!choice || !choice.message) {
        return res.json({ error: 'no message in provider response' });
    }

    // Return the whole message: Code Chat needs `tool_calls` to continue.
    return res.json({ message: choice.message, usage: response.data.usage });
}


/*
════════════════════════════════════════════════════════════════════════════
IMAGE GENERATION VIA OPENAI

Answered in one call, like OpenRouter — no queue, so no request id to poll.
The one difference is that OpenAI splits the two operations across endpoints:
/images/generations for a new image, /images/edits for one derived from a
reference. The route picks between them by whether references were sent.

An edit takes multipart form data rather than JSON, so a reference arriving as
a data URL is decoded back to bytes here.

Either way the API answers with base64. It is saved here and only a URL goes
back to the browser — base64 in the page would be written into every save of
that content.
════════════════════════════════════════════════════════════════════════════
*/
async function serveOpenAIImage(req, res) {
    const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
    const { imageModel, prompt, imageSize, input_references, customData } = req.body;

    const folderPath = (customData && customData.folderPath) || '';
    const imageQuality = 70;
    const references = Array.isArray(input_references) ? input_references : [];

    // A data URL or an http(s) URL, either way ending as bytes for the form.
    const toBuffer = async (url) => {
        if (/^data:/i.test(url)) return Buffer.from(url.split(',')[1] || '', 'base64');
        const r = await axios.get(url, { responseType: 'arraybuffer' });
        return Buffer.from(r.data);
    };

    let data;
    try {
        if (references.length) {
            // Required here rather than at the top: an edit is the only path
            // that needs it, and this keeps the helper self-contained.
            const FormData = require('form-data');
            const form = new FormData();
            form.append('model', imageModel);
            form.append('prompt', prompt || '');
            if (imageSize) form.append('size', imageSize);   // OpenAI takes pixels

            for (let i = 0; i < references.length; i++) {
                const buf = await toBuffer(references[i]);
                form.append('image[]', buf, { filename: `reference-${i + 1}.png`, contentType: 'image/png' });
            }

            const response = await axios.post('https://api.openai.com/v1/images/edits', form, {
                headers: { ...form.getHeaders(), Authorization: `Bearer ${OPENAI_API_KEY}` },
                maxBodyLength: Infinity,
            });
            data = response.data;
        } else {
            const payload = { model: imageModel, prompt: prompt || '' };
            if (imageSize) payload.size = imageSize;        // OpenAI takes pixels

            const response = await axios.post('https://api.openai.com/v1/images/generations', payload, {
                headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${OPENAI_API_KEY}` },
            });
            data = response.data;
        }
    } catch (e) {
        const d = e.response && e.response.data;
        const message = (d && d.error && d.error.message) || e.message;
        return res.json({ error: message, status: (e.response && e.response.status) || 0 });
    }

    const entry = data && data.data && data.data[0];
    if (!entry || !entry.b64_json) {
        return res.json({ error: 'The generation returned no image.' });
    }

    try {
        const imageData = Buffer.from(entry.b64_json, 'base64');
        const filename = 'ai-' + Math.random().toString(36).substring(2, 7) + '.jpg';

        if (localSave) {
            const filePath = path.join($path, folderPath, filename);
            await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toFile(filePath);
            return res.json({ url: $urlpath + path.posix.join(folderPath, filename) });
        }

        const buffer = await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toBuffer();
        const key = ($prefix ? $prefix + '/' : '') + path.posix.join(folderPath, filename);
        await s3.send(new PutObjectCommand({
            Bucket: process.env.AWS_BUCKET,
            Key: key,
            Body: buffer,
            ContentType: 'image/jpeg',
        }));
        return res.json({ url: `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}` });
    } catch (e) {
        return res.json({ error: 'Could not save the generated image: ' + e.message });
    }
}

app.post('/sendcommand', async (req, res) => {

    const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
    
    const DEFAULT_MODEL = 'gpt-4o-mini';

    // Image generation — one call, answered from the same route so a host
    // needs no extra endpoint. Neither v1 nor v2 sends `imageModel`.
    if (req.body && req.body.imageModel) {
        try {
            return await serveOpenAIImage(req, res);
        } catch (e) { return res.json({ error: e.message }); }
    }

    // chatEngine 'v2' — conversational shape. No `messages` = v1, unchanged.
    if (req.body && req.body.messages) {
        try {
            return await serveConversation(req, res, {
                url: 'https://api.openai.com/v1/chat/completions',
                apiKey: OPENAI_API_KEY,
                defaultModel: DEFAULT_MODEL,
            });
        } catch (e) { return res.json({ error: e.message }); }
    }

    let { question, context, system, functs, model } = req.body;

    const RESPONSE_ENDPOINT_MODELS = ['gpt-5.1-codex-mini', 'gpt-5.1-codex'];
    const useResponsesEndpoint = RESPONSE_ENDPOINT_MODELS.includes(model);

    const url = useResponsesEndpoint 
        ? 'https://api.openai.com/v1/responses'
        : 'https://api.openai.com/v1/chat/completions';

    const messages = [
        { role: 'system', content: system },
        { role: 'assistant', content: context || '' },
        { role: 'user', content: question }
    ];

    try {
        const requestBody = useResponsesEndpoint
            ? {
                model,
                input: messages,
            }
            : {
                model: model || DEFAULT_MODEL,
                messages,
                functions: functs.length === 0 ? undefined : functs
            };

        const response = await axios.post(url, requestBody, {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${OPENAI_API_KEY}`,
            },
        });

        let answer, usage;

        if(useResponsesEndpoint) {

            // Normalize v1/responses format to match v1/chat/completions
            const messageOutput = response.data.output.find(item => item.type === 'message');
            answer = {
                choices: [{
                    message: {
                        role: messageOutput.role,
                        content: messageOutput.content[0].text
                    }
                }]
            };
            usage = response.data.usage;

        } else {

            answer = functs.length === 0
            ? response.data
            : (response.data.choices[0].message.function_call ? 
                response.data.choices[0].message.function_call.arguments 
                : response.data.choices[0].message );
            usage = response.data.usage;

        }

        res.json({ answer, usage });
        
    } catch (error) {
        res.json({ error: error.message });
    }
});

/*
════════════════════════════════════════════════════════════════════════════
IMAGE GENERATION VIA OPENROUTER

One call, no queue: POST the prompt and the image comes back in the same
response. That is the whole difference from Fal, which answers with a request
id to poll and a result to fetch afterwards.

The API returns base64. It is saved here and only a URL goes back to the
browser — the same thing /result-fal does with Fal's hosted image — because
base64 in the page would be written into every save of that content.
════════════════════════════════════════════════════════════════════════════
*/
async function serveImageGeneration(req, res) {
    const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
    const { imageModel, prompt, imageSize, input_references, customData } = req.body;

    const folderPath = (customData && customData.folderPath) || '';
    const imageQuality = 70;

    const payload = { model: imageModel, prompt };
    if (imageSize) payload.aspect_ratio = imageSize;   // OpenRouter takes a ratio
    if (Array.isArray(input_references) && input_references.length) {
        payload.input_references = input_references.map(url => ({
            type: 'image_url',
            image_url: { url },
        }));
    }

    let data;
    try {
        const response = await axios.post('https://openrouter.ai/api/v1/images', payload, {
            headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${OPENROUTER_API_KEY}` },
        });
        data = response.data;
    } catch (e) {
        const d = e.response && e.response.data;
        const message = (d && d.error && d.error.message) || e.message;
        return res.json({ error: message, status: (e.response && e.response.status) || 0 });
    }

    const entry = data && data.data && data.data[0];
    if (!entry || !entry.b64_json) {
        return res.json({ error: 'The generation returned no image.' });
    }

    try {
        const imageData = Buffer.from(entry.b64_json, 'base64');
        const filename = 'ai-' + Math.random().toString(36).substring(2, 7) + '.jpg';

        if (localSave) {
            const filePath = path.join($path, folderPath, filename);
            await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toFile(filePath);
            return res.json({ url: $urlpath + path.posix.join(folderPath, filename) });
        }

        const buffer = await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toBuffer();
        const key = ($prefix ? $prefix + '/' : '') + path.posix.join(folderPath, filename);
        await s3.send(new PutObjectCommand({
            Bucket: process.env.AWS_BUCKET,
            Key: key,
            Body: buffer,
            ContentType: 'image/jpeg',
        }));
        return res.json({ url: `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}` });
    } catch (e) {
        return res.json({ error: 'Could not save the generated image: ' + e.message });
    }
}

app.post('/openrouter', async (req, res) => {

    try {
        const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
        
        const url = "https://openrouter.ai/api/v1/chat/completions";
        const DEFAULT_MODEL = 'openai/gpt-4o-mini'; // Default model for content generation
        const DEFAULT_TEMPERATURE = 0.6;
        const DEFAULT_TOP_P = 0.9;
        const DEFAULT_NUM = 1;

        // Image generation — one call, answered from the same route so a host
        // needs no extra endpoint. Neither v1 nor v2 sends `imageModel`.
        if (req.body && req.body.imageModel) {
            try {
                return await serveImageGeneration(req, res);
            } catch (e) { return res.json({ error: e.message }); }
        }

        // chatEngine 'v2' — conversational shape. No `messages` = v1, unchanged.
        if (req.body && req.body.messages) {
            try {
                return await serveConversation(req, res, {
                    url,
                    apiKey: OPENROUTER_API_KEY,
                    defaultModel: DEFAULT_MODEL,
                });
            } catch (e) { return res.json({ error: e.message }); }
        }

        let { question, context, system, functs, temperature, topP, num, model } = req.body;

        const messages = [
            { role: 'system', content: system },
            { role: 'assistant', content: context || '' },
            { role: 'user', content: question }
        ];

        try {

            if(functs.length>0) { 

                // --- Function Calling Branch ---
                // For function calling, only certain models are recommended.
                // The model is used solely for request categorization. No impact on content generation.

                const tools = [
                    {
                        type: "function",
                        function: {
                            ...functs[0]
                        }
                    }
                ];

                const response = await axios.post(url, {
                    model: 'anthropic/claude-3.5-sonnet',
                    /*
                    Models for function calling:
                    - anthropic/claude-3.5-sonnet
                    - qwen/qwen-2.5-72b-instruct
                    */
                    messages,
                    temperature: parseFloat(temperature) || DEFAULT_TEMPERATURE,
                    top_p: parseFloat(topP) || DEFAULT_TOP_P,
                    n: parseInt(num) || DEFAULT_NUM,
                    tools,
                }, {
                    headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
                    },
                    validateStatus: () => true,
                });

                if(response.data && response.data.error && response.data.error.message) {
                    res.json({ ok: false, status: 404, error: response.data.error.message });
                    return;
                }

                let answer =
                    functs.length === 0
                        ? response.data
                        : response.data.choices[0].message.tool_calls
                        ? response.data.choices[0].message.tool_calls[0].function.arguments
                        : response.data.choices[0].message;
                let usage = response.data.usage;

                res.json({ answer, usage });

            } else {

                // --- Content Generation Branch ---

                const response = await axios.post(url, {
                    model: model || DEFAULT_MODEL,
                    messages,
                    temperature: parseFloat(temperature) || DEFAULT_TEMPERATURE,
                    top_p: parseFloat(topP) || DEFAULT_TOP_P,
                    n: parseInt(num) || DEFAULT_NUM
                }, {
                    headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
                    },
                });

                let answer = response.data;
                let usage = response.data.usage;
    
                res.json({ answer, usage });

            }
            
        } catch (error) {
            res.json({ ok: false, status: 500, error: error.message }); 
        }

    } catch(e) {
        res.status(200).json({ ok: true, status: 500, error: 'Something went wrong.' });
    }
});

Using OpenAI

Replace this in your server file:

app.post('/sendcommand', async (req, res) => {
    ...
});

with this:

/*
════════════════════════════════════════════════════════════════════════════
IMAGE GENERATION VIA OPENAI

Answered in one call, like OpenRouter — no queue, so no request id to poll.
The one difference is that OpenAI splits the two operations across endpoints:
/images/generations for a new image, /images/edits for one derived from a
reference. The route picks between them by whether references were sent.

An edit takes multipart form data rather than JSON, so a reference arriving as
a data URL is decoded back to bytes here.

Either way the API answers with base64. It is saved here and only a URL goes
back to the browser — base64 in the page would be written into every save of
that content.
════════════════════════════════════════════════════════════════════════════
*/
async function serveOpenAIImage(req, res) {
    const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
    const { imageModel, prompt, imageSize, input_references, customData } = req.body;

    const folderPath = (customData && customData.folderPath) || '';
    const imageQuality = 70;
    const references = Array.isArray(input_references) ? input_references : [];

    // A data URL or an http(s) URL, either way ending as bytes for the form.
    const toBuffer = async (url) => {
        if (/^data:/i.test(url)) return Buffer.from(url.split(',')[1] || '', 'base64');
        const r = await axios.get(url, { responseType: 'arraybuffer' });
        return Buffer.from(r.data);
    };

    let data;
    try {
        if (references.length) {
            // Required here rather than at the top: an edit is the only path
            // that needs it, and this keeps the helper self-contained.
            const FormData = require('form-data');
            const form = new FormData();
            form.append('model', imageModel);
            form.append('prompt', prompt || '');
            if (imageSize) form.append('size', imageSize);   // OpenAI takes pixels

            for (let i = 0; i < references.length; i++) {
                const buf = await toBuffer(references[i]);
                form.append('image[]', buf, { filename: `reference-${i + 1}.png`, contentType: 'image/png' });
            }

            const response = await axios.post('https://api.openai.com/v1/images/edits', form, {
                headers: { ...form.getHeaders(), Authorization: `Bearer ${OPENAI_API_KEY}` },
                maxBodyLength: Infinity,
            });
            data = response.data;
        } else {
            const payload = { model: imageModel, prompt: prompt || '' };
            if (imageSize) payload.size = imageSize;        // OpenAI takes pixels

            const response = await axios.post('https://api.openai.com/v1/images/generations', payload, {
                headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${OPENAI_API_KEY}` },
            });
            data = response.data;
        }
    } catch (e) {
        const d = e.response && e.response.data;
        const message = (d && d.error && d.error.message) || e.message;
        return res.json({ error: message, status: (e.response && e.response.status) || 0 });
    }

    const entry = data && data.data && data.data[0];
    if (!entry || !entry.b64_json) {
        return res.json({ error: 'The generation returned no image.' });
    }

    try {
        const imageData = Buffer.from(entry.b64_json, 'base64');
        const filename = 'ai-' + Math.random().toString(36).substring(2, 7) + '.jpg';

        if (localSave) {
            const filePath = path.join($path, folderPath, filename);
            await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toFile(filePath);
            return res.json({ url: $urlpath + path.posix.join(folderPath, filename) });
        }

        const buffer = await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toBuffer();
        const key = ($prefix ? $prefix + '/' : '') + path.posix.join(folderPath, filename);
        await s3.send(new PutObjectCommand({
            Bucket: process.env.AWS_BUCKET,
            Key: key,
            Body: buffer,
            ContentType: 'image/jpeg',
        }));
        return res.json({ url: `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}` });
    } catch (e) {
        return res.json({ error: 'Could not save the generated image: ' + e.message });
    }
}

app.post('/sendcommand', async (req, res) => {

    const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
    
    const DEFAULT_MODEL = 'gpt-4o-mini';

    // Image generation — one call, answered from the same route so a host
    // needs no extra endpoint. Neither v1 nor v2 sends `imageModel`.
    if (req.body && req.body.imageModel) {
        try {
            return await serveOpenAIImage(req, res);
        } catch (e) { return res.json({ error: e.message }); }
    }

    // chatEngine 'v2' — conversational shape. No `messages` = v1, unchanged.
    if (req.body && req.body.messages) {
        try {
            return await serveConversation(req, res, {
                url: 'https://api.openai.com/v1/chat/completions',
                apiKey: OPENAI_API_KEY,
                defaultModel: DEFAULT_MODEL,
            });
        } catch (e) { return res.json({ error: e.message }); }
    }

    let { question, context, system, functs, model } = req.body;

    const RESPONSE_ENDPOINT_MODELS = ['gpt-5.1-codex-mini', 'gpt-5.1-codex'];
    const useResponsesEndpoint = RESPONSE_ENDPOINT_MODELS.includes(model);

    const url = useResponsesEndpoint 
        ? 'https://api.openai.com/v1/responses'
        : 'https://api.openai.com/v1/chat/completions';

    const messages = [
        { role: 'system', content: system },
        { role: 'assistant', content: context || '' },
        { role: 'user', content: question }
    ];

    try {
        const requestBody = useResponsesEndpoint
            ? {
                model,
                input: messages,
            }
            : {
                model: model || DEFAULT_MODEL,
                messages,
                functions: functs.length === 0 ? undefined : functs
            };

        const response = await axios.post(url, requestBody, {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${OPENAI_API_KEY}`,
            },
        });

        let answer, usage;

        if(useResponsesEndpoint) {

            // Normalize v1/responses format to match v1/chat/completions
            const messageOutput = response.data.output.find(item => item.type === 'message');
            answer = {
                choices: [{
                    message: {
                        role: messageOutput.role,
                        content: messageOutput.content[0].text
                    }
                }]
            };
            usage = response.data.usage;

        } else {

            answer = functs.length === 0
            ? response.data
            : (response.data.choices[0].message.function_call ? 
                response.data.choices[0].message.function_call.arguments 
                : response.data.choices[0].message );
            usage = response.data.usage;

        }

        res.json({ answer, usage });
        
    } catch (error) {
        res.json({ error: error.message });
    }
});

/*
════════════════════════════════════════════════════════════════════════════
IMAGE GENERATION VIA OPENROUTER

One call, no queue: POST the prompt and the image comes back in the same
response. That is the whole difference from Fal, which answers with a request
id to poll and a result to fetch afterwards.

The API returns base64. It is saved here and only a URL goes back to the
browser — the same thing /result-fal does with Fal's hosted image — because
base64 in the page would be written into every save of that content.
════════════════════════════════════════════════════════════════════════════
*/
async function serveImageGeneration(req, res) {
    const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
    const { imageModel, prompt, imageSize, input_references, customData } = req.body;

    const folderPath = (customData && customData.folderPath) || '';
    const imageQuality = 70;

    const payload = { model: imageModel, prompt };
    if (imageSize) payload.aspect_ratio = imageSize;   // OpenRouter takes a ratio
    if (Array.isArray(input_references) && input_references.length) {
        payload.input_references = input_references.map(url => ({
            type: 'image_url',
            image_url: { url },
        }));
    }

    let data;
    try {
        const response = await axios.post('https://openrouter.ai/api/v1/images', payload, {
            headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${OPENROUTER_API_KEY}` },
        });
        data = response.data;
    } catch (e) {
        const d = e.response && e.response.data;
        const message = (d && d.error && d.error.message) || e.message;
        return res.json({ error: message, status: (e.response && e.response.status) || 0 });
    }

    const entry = data && data.data && data.data[0];
    if (!entry || !entry.b64_json) {
        return res.json({ error: 'The generation returned no image.' });
    }

    try {
        const imageData = Buffer.from(entry.b64_json, 'base64');
        const filename = 'ai-' + Math.random().toString(36).substring(2, 7) + '.jpg';

        if (localSave) {
            const filePath = path.join($path, folderPath, filename);
            await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toFile(filePath);
            return res.json({ url: $urlpath + path.posix.join(folderPath, filename) });
        }

        const buffer = await sharp(imageData).jpeg({ quality: imageQuality }).withMetadata(false).toBuffer();
        const key = ($prefix ? $prefix + '/' : '') + path.posix.join(folderPath, filename);
        await s3.send(new PutObjectCommand({
            Bucket: process.env.AWS_BUCKET,
            Key: key,
            Body: buffer,
            ContentType: 'image/jpeg',
        }));
        return res.json({ url: `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}` });
    } catch (e) {
        return res.json({ error: 'Could not save the generated image: ' + e.message });
    }
}

Using both? Paste the conversation helpers once, then both image helpers and both routes.


View this page on GitHub

© 2026 InnovaStudio

This site is built with Site Builder Kit