Open editor →

Custom endpoint

Point a graph at http://127.0.0.1:PORT/v1/chat/completions (or another allowed host). The fetch leaves the browser directly — nanoodle never proxies it, and never sends your NanoGPT key.

What the node does

Add Custom endpoint from the Text group. Set the URL and a mode (chat, image, video, audio, or json). The output port follows the mode so a chat result wires like an LLM, an image result like an Image node, and so on.

The node always shows the live contract: Nanoodle will POST (the request body for the current mode) and Your server should return (the NanoGPT-shaped JSON). An optional Authorization value is yours alone — it is blanked from share links and exports.

Your server must send Access-Control-Allow-Origin for the editor origin (and nanoodle.com / the play origin) and answer OPTIONS. If the browser blocks the call, the node says so in one line.

A 20-line localhost chat server

// node server.mjs  →  http://127.0.0.1:8787/v1/chat/completions
import http from "node:http";
const cors = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "content-type,authorization",
  "Access-Control-Allow-Methods": "POST,OPTIONS",
};
http.createServer((req, res) => {
  if (req.method === "OPTIONS") { res.writeHead(204, cors); return res.end(); }
  if (req.method !== "POST") { res.writeHead(404, cors); return res.end(); }
  let b = ""; req.on("data", c => b += c);
  req.on("end", () => {
    const j = JSON.parse(b || "{}");
    const last = (j.messages || []).at(-1);
    const text = typeof last?.content === "string" ? last.content : "ok";
    res.writeHead(200, { ...cors, "Content-Type": "application/json" });
    res.end(JSON.stringify({ choices: [{ message: { content: "echo: " + text } }] }));
  });
}).listen(8787, "127.0.0.1");

Python (same contract):

# python3 -m pip is not required — stdlib only
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class H(BaseHTTPRequestHandler):
    def _cors(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Headers", "content-type,authorization")
        self.send_header("Access-Control-Allow-Methods", "POST,OPTIONS")
    def do_OPTIONS(self):
        self.send_response(204); self._cors(); self.end_headers()
    def do_POST(self):
        n = int(self.headers.get("Content-Length") or 0)
        j = json.loads(self.rfile.read(n) or b"{}")
        last = (j.get("messages") or [{}])[-1]
        text = last.get("content") if isinstance(last.get("content"), str) else "ok"
        body = json.dumps({"choices":[{"message":{"content":"echo: "+str(text)}}]}).encode()
        self.send_response(200); self._cors()
        self.send_header("Content-Type", "application/json"); self.end_headers()
        self.wfile.write(body)
HTTPServer(("127.0.0.1", 8787), H).serve_forever()

In the editor: URL http://127.0.0.1:8787/v1/chat/completions, mode chat, wire a Text node into prompt, Run. The downstream text port should light up with echo: ….