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
/*
Models:
- openai/gpt-4o-mini
- anthropic/claude-3.5-sonnet
- qwen/qwen-2.5-72b-instruct
- deepseek/deepseek-r1-distill-llama-70b
- meta-llama/llama-3.3-70b-instruct
For more models: https://openrouter.ai/models
Note: Different models produce different results.
Please test the model to ensure it aligns with your preferences, as responses can vary in style, depth, and accuracy.
*/
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.