Next.js

1. Add this file as src/lib/serveConversation.ts

// src/lib/serveConversation.ts
/*
────────────────────────────────────────────────────────────────────────────
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.
────────────────────────────────────────────────────────────────────────────
*/
import { NextResponse } from 'next/server';

type Body = {
    messages?: unknown;
    tools?: Array<Record<string, unknown>>;
    system?: string;
    model?: string;
    temperature?: number | string;
    topP?: number | string;
    cacheContext?: boolean;
    stream?: boolean;
};

/*
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 markConversationCacheable(messages: unknown[]): unknown[] {
    const out = messages.slice() as Array<Record<string, unknown>>;
    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 as Array<Record<string, unknown>>).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;
}

export async function serveConversation(
    body: Body,
    { url, apiKey, defaultModel }: { url: string; apiKey?: string; defaultModel: string }
) {
    const messages = body.messages;

    if (!Array.isArray(messages) || messages.length === 0) {
        return NextResponse.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 = !!body.system && body.system.length > 2000 && body.cacheContext !== false;
    const systemMessage = cacheable
        ? { role: 'system', content: [{ type: 'text', text: body.system, cache_control: { type: 'ephemeral' } }] }
        : { role: 'system', content: body.system };

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

    const withSystem = body.system ? [systemMessage, ...conversation] : conversation;

    const payload: Record<string, unknown> = {
        model: body.model || defaultModel,
        messages: withSystem,
        temperature: body.temperature === undefined ? 0.7 : parseFloat(String(body.temperature)),
        top_p: body.topP === undefined ? 0.9 : parseFloat(String(body.topP)),
    };

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

    // Streams the reply back so the answer appears as it is written.
    // Used automatically when the request has `stream: true`.
    if (body.stream) {
        payload.stream = true;
        payload.stream_options = { include_usage: true };
    }

    const send = () => fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiKey}`,
        },
        body: JSON.stringify(payload),
    });

    let response = await send();
    let errorText = '';

    if (response.status >= 400) {
        errorText = await response.text();

        /*
        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.
        */
        if (/reasoning_effort/i.test(errorText) && payload.reasoning_effort === undefined) {
            payload.reasoning_effort = 'none';
            response = await send();
            errorText = response.status >= 400 ? await response.text() : '';
        }
    }

    // Pass the provider's status through, so Code Chat waits and retries
    // when you are rate limited instead of giving up.
    if (response.status >= 400) {
        let message = `Request failed with status code ${response.status}`;
        try {
            const body = JSON.parse(errorText);
            if (body?.error?.message) message = body.error.message;
        } catch { /* not JSON */ }
        const retryAfter = response.headers.get('retry-after');
        return NextResponse.json({
            error: message,
            status: response.status,
            ...(retryAfter ? { retryAfter: Number(retryAfter) } : {}),
        });
    }

    if (body.stream && response.body) {
        return new NextResponse(response.body, {
            headers: {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache',
                'X-Accel-Buffering': 'no',
            },
        });
    }

    const data = await response.json();

    if (data?.error) {
        return NextResponse.json({ error: data.error.message || String(data.error) });
    }

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

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

2. Add this file as src/lib/serveImageGeneration.ts

// src/lib/serveImageGeneration.ts
//
// Image generation via OpenRouter — one call, no queue.
//
// Unlike Fal there is no request id to poll and no result to fetch: the image
// comes back in the same response. It arrives as base64 and is saved here, so
// the browser only ever receives a URL — base64 in the page would be written
// into every save of that content.

import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs';

const uploadPath = process.env.UPLOAD_PATH || '';   // Physical path (e.g. "/public/uploads/")
const uploadUrl = process.env.UPLOAD_URL || '';     // URL base (e.g. "/uploads/")

function randomName() {
    return 'ai-' + Math.random().toString(36).substring(2, 7);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function serveImageGeneration(body: any, apiKey?: string, provider: 'openrouter' | 'openai' = 'openrouter') {
    const references: string[] = Array.isArray(body.input_references) ? body.input_references : [];
    const isOpenAI = provider === 'openai';

    let response: Response;

    if (isOpenAI && references.length) {
        /*
        OpenAI is the only one that splits the two operations: an image derived
        from a reference goes to /images/edits as multipart form data, not JSON.
        A reference arriving as a data URL is decoded back to bytes for the form.
        */
        const form = new FormData();
        form.append('model', body.imageModel);
        form.append('prompt', body.prompt || '');
        if (body.imageSize) form.append('size', body.imageSize);      // OpenAI takes pixels

        for (let i = 0; i < references.length; i++) {
            const url = references[i];
            const blob = /^data:/i.test(url)
                ? await (await fetch(url)).blob()
                : await (await fetch(url)).blob();
            form.append('image[]', blob, `reference-${i + 1}.png`);
        }

        response = await fetch('https://api.openai.com/v1/images/edits', {
            method: 'POST',
            headers: { Authorization: `Bearer ${apiKey}` },
            body: form,
        });
    } else {
        const payload: Record<string, unknown> = {
            model: body.imageModel,
            prompt: body.prompt || '',
        };
        // OpenRouter takes an aspect ratio, OpenAI takes pixels
        if (body.imageSize) payload[isOpenAI ? 'size' : 'aspect_ratio'] = body.imageSize;
        if (!isOpenAI && references.length) {
            payload.input_references = references.map((url: string) => ({
                type: 'image_url',
                image_url: { url },
            }));
        }

        response = await fetch(
            isOpenAI ? 'https://api.openai.com/v1/images/generations' : 'https://openrouter.ai/api/v1/images',
            {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
                body: JSON.stringify(payload),
            });
    }

    const data = await response.json();

    if (!response.ok) {
        return NextResponse.json({
            error: data?.error?.message || `Request failed with status code ${response.status}`,
            status: response.status,
        });
    }

    const b64 = data?.data?.[0]?.b64_json;
    if (!b64) return NextResponse.json({ error: 'The generation returned no image.' });

    try {
        const folderPath = body?.customData?.folderPath || '';
        const filename = `${randomName()}.jpg`;
        const filePath = path.join(process.cwd(), uploadPath, folderPath, filename);

        fs.mkdirSync(path.dirname(filePath), { recursive: true });
        fs.writeFileSync(filePath, Buffer.from(b64, 'base64'));

        return NextResponse.json({ url: uploadUrl + path.posix.join(folderPath, filename) });
    } catch (e) {
        return NextResponse.json({ error: 'Could not save the generated image: ' + (e as Error).message });
    }
}

3. Using OpenRouter — replace src/app/api/openrouter/route.ts with this

// src/app/api/openrouter/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { serveConversation } from '@/lib/serveConversation';
import { serveImageGeneration } from '@/lib/serveImageGeneration';

const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;

export async function POST(req: NextRequest) {
    try {
        const url = "https://openrouter.ai/api/v1/chat/completions";
        const DEFAULT_MODEL = 'openai/gpt-4o-mini';
        const DEFAULT_TEMPERATURE = 0.6;
        const DEFAULT_TOP_P = 0.9;
        const DEFAULT_NUM = 1;

        const body = await req.json();

        // Image generation — one call, answered from the same route so a host
        // needs no extra endpoint. Neither v1 nor v2 sends `imageModel`.
        if (body?.imageModel) {
            return await serveImageGeneration(body, OPENROUTER_API_KEY);
        }

        // chatEngine 'v2' — conversational shape. No `messages` = v1, unchanged.
        if (body?.messages) {
            return await serveConversation(body, {
                url,
                apiKey: OPENROUTER_API_KEY,
                defaultModel: DEFAULT_MODEL,
            });
        }

        const { question, context, system, functs = [], temperature, topP, num, model } = body;

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

        try {
            if (functs.length > 0) {
                // --- Function Calling Branch ---

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

                const response = await fetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
                    },
                    body: JSON.stringify({
                        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,
                    })
                });

                const data = await response.json();

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

                const answer = functs.length === 0
                    ? data
                    : (data.choices[0].message.tool_calls
                        ? data.choices[0].message.tool_calls[0].function.arguments
                        : data.choices[0].message);
                const usage = data.usage;

                return NextResponse.json({ answer, usage });

            } else {
                // --- Content Generation Branch ---

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

                const data = await response.json();
                const answer = data;
                const usage = data.usage;

                return NextResponse.json({ answer, usage });
            }

        } catch (error) {
            return NextResponse.json(
                { ok: false, status: 500, error: error instanceof Error ? error.message : 'Unknown error' },
                { status: 200 }
            );
        }

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

4. Using OpenAI — replace src/app/api/openai/route.ts with this

// src/app/api/openai/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { serveConversation } from '@/lib/serveConversation';
import { serveImageGeneration } from '@/lib/serveImageGeneration';

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;

export async function POST(req: NextRequest) {
    try {
        const DEFAULT_MODEL = 'gpt-4o-mini';

        const body = await req.json();

        // Image generation — one call, answered from the same route so a host
        // needs no extra endpoint. Neither v1 nor v2 sends `imageModel`.
        if (body?.imageModel) {
            return await serveImageGeneration(body, OPENAI_API_KEY, 'openai');
        }

        // chatEngine 'v2' — conversational shape. No `messages` = v1, unchanged.
        // v2 always uses chat/completions, never the v1/responses endpoint.
        if (body?.messages) {
            return await serveConversation(body, {
                url: 'https://api.openai.com/v1/chat/completions',
                apiKey: OPENAI_API_KEY,
                defaultModel: DEFAULT_MODEL,
            });
        }

        const { question, context, system, functs = [], model } = 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 }
        ];

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

        const openAiResponse = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${OPENAI_API_KEY}`
            },
            body: JSON.stringify(requestBody)
        });

        if (!openAiResponse.ok) {
            const errorData = await openAiResponse.json();
            return NextResponse.json({ error: errorData }, { status: openAiResponse.status });
        }

        const data = await openAiResponse.json();

        let answer, usage;

        if (useResponsesEndpoint) {
            // Normalize v1/responses format to match v1/chat/completions
            const messageOutput = data.output.find((item: any) => item.type === 'message');
            answer = {
                choices: [{
                    message: {
                        role: messageOutput.role,
                        content: messageOutput.content[0].text
                    }
                }]
            };
            usage = data.usage;
        } else {
            const choices = data.choices;
            answer = functs.length === 0
                ? data
                : (choices[0]?.message?.function_call
                    ? choices[0].message.function_call.arguments
                    : choices[0]?.message);
            usage = data.usage;
        }

        return NextResponse.json({ answer, usage });

    } catch (err: unknown) {
        return NextResponse.json(
            { ok: false, error: err instanceof Error ? err.message : "Unknown error" },
            { status: 500 }
        );
    }
}

View this page on GitHub

© 2026 InnovaStudio

This site is built with Site Builder Kit