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: ….