NodeTool provides both OpenAI-compatible HTTP endpoints and a WebSocket endpoint for chat interactions:
- The single server (
nodetool serve, defaults to port 7777) exposes OpenAI-compatible HTTP endpoints (/v1/chat/completions,/v1/models) for remote clients. - The same server exposes a WebSocket endpoint at
/wsthat carries both chat and workflow messages.
See the canonical matrix in API Reference for methods, auth requirements, and streaming behavior.
Port Reference: Both development and production deployments use port 7777 by default. Replace
localhostwith your server hostname for remote connections.
OpenAI-Compatible HTTP API
NodeTool exposes OpenAI-compatible endpoints that allow you to use standard OpenAI client libraries and tools.
When AUTH_PROVIDER is static or supabase, send Authorization: Bearer <token>; in local/none modes the token
is optional for development.
Chat Completions: POST /v1/chat/completions
URL: http://localhost:7777/v1/chat/completions
Headers:
Content-Type: application/jsonAuthorization: Bearer YOUR_TOKEN
Request Body:
{
"model": "gpt-5.6",
"messages": [
{"role": "user", "content": "Hello, how are you?"}
],
"stream": true
}
Request fields the server reads: model, messages, stream, tools,
temperature, top_p, max_tokens, presence_penalty, and
frequency_penalty. Other OpenAI fields are ignored. Messages can have the
roles system, user, assistant, and tool, and assistant messages can
carry tool_calls. stream defaults to false.
Provider selection. The server picks the provider from the model name:
gpt-*, o1*, and o3* go to OpenAI, claude-* goes to Anthropic, and
everything else goes to Ollama at OLLAMA_API_URL. The OpenAI and Anthropic keys
come from the caller’s stored secrets (OPENAI_API_KEY, ANTHROPIC_API_KEY).
A request without model uses llama3.2:latest.
Responses. Without streaming, the reply is a chat.completion object whose
finish_reason is stop or tool_calls. With "stream": true the reply is
text/event-stream of chat.completion.chunk objects, ending with a chunk that
carries finish_reason and then data: [DONE]. A failure while streaming is
sent as an error event object followed by [DONE]. usage is always zeros,
because the endpoint does not count tokens.
Errors use the OpenAI shape, { "error": { "message", "type", "param", "code" } }.
An unparsable body is a 400, and a provider that cannot start or a failed
non-streaming call is a 500 with type server_error.
Example using OpenAI Python client:
import openai
# For local development: base_url="http://localhost:7777/v1"
# For production/server: base_url="http://localhost:7777/v1" or your server URL
client = openai.OpenAI(
api_key="YOUR_TOKEN",
base_url="http://localhost:7777/v1"
)
response = client.chat.completions.create(
model="gpt-5.6",
messages=[
{"role": "user", "content": "Hello, how are you?"}
],
stream=True
)
for chunk in response:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="")
Models: GET /v1/models
URL: http://localhost:7777/v1/models
Returns a fixed list in the OpenAI list shape. It does not query a provider
and does not reflect which keys are configured. The entries are
llama3.2:latest, gpt-4, gpt-4o, gpt-4o-mini, claude-sonnet-4-20250514,
and claude-opus-4-20250514. A chat request accepts any model name the three
providers know, not only these. For the live catalog, use the
SDK models route.
# For local development and production: http://localhost:7777/v1/models
curl http://localhost:7777/v1/models \
-H "Authorization: Bearer YOUR_TOKEN"
WebSocket API
NodeTool also exposes a /ws WebSocket endpoint for real time conversations. The server side is implemented by
WebSocketClientSession which handles message parsing, tool execution and streaming responses.
The connection supports both binary (MessagePack) and text (JSON) messages. Authentication can be provided via
Authorization: Bearer <token> headers or an api_key query parameter.
Note: The WebSocket endpoint is served on port 7777 (development) or your configured server port.
WebSocket Example usage
Every message is a { command, data } envelope. A chat turn is the
chat_message command, and thread_id, provider, and model are required.
A bare chat payload gets an invalid_message error.
// For local development: ws://localhost:7777/ws
// For production deployments, use your server URL
const socket = new WebSocket("ws://localhost:7777/ws?api_key=YOUR_KEY");
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
const data = msgpack.decode(new Uint8Array(event.data));
if (data.type === "chunk" && data.content_type === "text") {
console.log(data.content);
}
};
socket.onopen = () => {
socket.send(
msgpack.encode({
command: "chat_message",
data: {
thread_id: "my-thread",
role: "user",
content: "Hello world",
provider: "openai",
model: "gpt-5-mini"
}
})
);
};
The server saves the user message to the thread, then streams the turn.
Replies are binary MessagePack by default. Send set_mode with "mode": "text"
first to receive JSON. The WebSocket API lists
the other chat_message fields.
Server responses
A turn streams these frames, each tagged with the thread_id:
chunk– streamed text from the model (content_type: "text"), withdone: trueon the last one andthinking: truefor reasoning textmessage– a persisted assistant or tool messagetool_call_updateandtool_result_update– a server-side tool call and its resulttool_call– a tool the client owns. Reply with atool_resultframetool_approval_request– a gated tool call. Reply withtool_approval_responseanddecisionofallow,allow_for_chat, ordenysecret_request– the model needs a credential. Reply withsecret_request_responsetask_update,planning_update,todo_update– agent progresserror– the turn failed
Set workflow_target: "workflow" on chat_message to run a workflow as the
chat agent. That path also streams job_update frames. A bare workflow_id is only
context and does not start a run.
Reconnecting to a turn
A turn keeps running if the socket drops. After reconnecting, send
list_chat_turns to find running turns, then resume_chat with the thread id and
the highest chat_seq you received. The server replays the frames you missed.
Send stop with the thread_id to cancel a turn.