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), or wire those fields from a Text or Choice node. The output port follows the mode so a chat result wires like an LLM, an image result like an Image node, and so on. A Choice option like /post replaces the path on the node's host. A full route shaped chat · http://127.0.0.1:8787/v1/chat/completions (mode, a separator, then the URL) sets both and retargets the POST — it is not a label in the body.

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.

Chrome local-network access and CORS

From https://nanoodle.com, Chrome treats localhost as a local network. The first Run may prompt to allow local-network access — click Allow, or the fetch fails before it reaches your server. Separately, the server must send Access-Control-Allow-Origin (the editor origin, or *) and answer OPTIONS. If the browser blocks the call, the node names CORS or Chrome local-network permission when it can tell.

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: …. Chrome may prompt once for local-network access — that is the browser, not these servers.