Package Information
Available Nodes
Documentation generating…
Documentation
n8n-nodes-rexai
n8n nodes for RexAi — an OpenAI-compatible proxy that fronts
ChatGPT, Z-AI, DeepSeek, Gemini and Qwen accounts.
Two nodes, one package:
| Node | Shape on the canvas | What it is |
|---|---|---|
| RexAi Chat Model | circle | A LangChain chat model sub-node. Connect it to the AI Agent node's Chat Model input. |
| RexAi | rounded square | A standalone node: Chat, Image Analysis, Image Generation, Analyze Audio, Transcribe Audio. |
They ship together because they share one credential. n8n resolves a credential by
its name, so splitting them into two packages would mean either duplicatingrexAiApi (which collides in the credential registry) or making one package
depend on the other. The canvas shape is not a packaging decision at all: a
sub-node with an AiLanguageModel output renders as a circle, an execute() node
renders as a square, and both live happily in one package.
Install
npm install n8n-nodes-rexai
Or, from this directory, to link it into a local n8n:
npm install
npm run build
npm link
# in your n8n install:
npm link n8n-nodes-rexai
Credentials
One credential type, RexAi API:
| Field | Default | Notes |
|---|---|---|
| Base URL | https://ai.rexlab.app/v1 |
Changeable. Point it at your own instance to self-host. Keep the /v1 suffix. |
| API Key | — | Sent as Authorization: Bearer <key>. |
The Test button calls GET /v1/models, which is the cheapest authenticated
endpoint RexAi serves and the same one the model picker reads.
Models
The Model field is a dropdown, loaded live from GET /v1/models on the base URL
in your credential — whatever your instance exposes is what you can pick. Both nodes
use the same picker, from one implementation, so they cannot offer different lists.
The default is rexai-auto, RexAi's own router id, which rotates across whatever
accounts are live. It is the one choice that cannot go stale when an account is added
or a model retired. Two caveats:
- RexAi only lists and accepts
rexai-autowhile auto mode is on — i.e.modein~/.rexai/auto.jsonisshuffle(the default) orround-robin, notoff. - A plain API key that is not scoped to auto does not see it either, and such a
key must pick a concrete model./v1/modelsis authoritative here and the dropdown
mirrors it exactly; nothing is invented locally.
If the server cannot be reached the dropdown falls back to a short offline list
(rexai-auto, deepseek-flash, deepseek-pro, deepseek-vision), so the field is
never empty while you are still typing the Base URL.
The session header
Both nodes expose RexAi Session Header. You supply the value only — the
node sends it as the x-rexai-session header.
- Leave it empty for a stateless request.
- Set a fixed value (e.g.
my-conversation) to pin a conversation. - Or make it dynamic with an expression —
{{ $json.sessionId }}— so each
item keeps its own conversation.
Leaving it empty is deliberately different from setting it to "": an empty
header would ask RexAi for a chained conversation keyed on the empty string,
rather than a stateless request. The node omits the header entirely when the
field is blank.
Why the Chat Model is visible on the canvas
A sub-node is never scheduled. Nothing in the execution engine runs it as a step —
whatever consumes its ai_languageModel output pulls it in on demand — so n8n never
fires nodeExecuteBefore / nodeExecuteAfter for it and it gets no run data of its
own. That is why a sub-node with nothing extra is a circle that never lights up while
the agent runs and never shows what the model answered.
Both symptoms have one cause and one fix. RexAiTracer (nodes/shared/tracing.ts) is
attached as a LangChain callback in supplyData, and it does the two things that make
the node visible:
| Call | Effect |
|---|---|
addInputData(connectionType, ...) on LLM start |
stamps the run running, fires nodeExecuteBefore → the animated border |
addOutputData(connectionType, index, ...) on LLM end |
stamps it success, attaches the payload → the data shown inside the node |
connectionType must be the node's own ai_languageModel. Run data is filed under
that key, so a mismatch writes it somewhere the frontend never looks — the same
silence as writing nothing at all.
n8n's built-in LLM sub-nodes get this from N8nLlmTracing in @n8n/ai-utilities,
which is not usable here: that package is licensed under n8n's Sustainable Use
License, not something a published community node may depend on. RexAiTracer is the
same behaviour written against the public ISupplyDataFunctions API ofn8n-workflow — which is exactly what @n8n/ai-utilities itself calls.
Do not remove the callbacks: [new RexAiTracer(this)] line. Nothing else fails if you
do: the node still answers the agent, it just goes dark.
Operations
| Operation | Endpoint |
|---|---|
| Chat | POST /v1/chat/completions |
| Image Analysis | POST /v1/chat/completions (vision, as image_url content parts) |
| Image Generation | POST /v1/images/generations |
| Analyze Audio | POST /v1/chat/completions or POST /v1/responses (audio, as an input_audio part) |
| Transcribe Audio | POST /v1/openai-transcription or POST /v1/gemini-transcription (multipart) |
Transcription uses non-standard paths. RexAi does not serve
/v1/audio/transcriptions. The node offers the two real endpoints explicitly.
A node written from the OpenAI spec would 404 here.
For Image Analysis, the image can come from a public URL (or a data: URI) or
from the incoming item's binary data — the binary is inlined as a data: URI, so
nothing has to be uploaded first.
Analyze Audio vs Transcribe Audio
Two different jobs, two different operations. This split is new in 0.2.5, and
it is a behaviour change: a node saved as Audio Analysis transcribes today and
analyses from 0.2.5, so re-pick the operation on any workflow you already have.
| Analyze Audio | Transcribe Audio | |
|---|---|---|
| Asks | a question about the recording | nothing — speech in, text out |
| Goes to | the chat surface, or /v1/responses |
a multipart transcription endpoint |
| Model list | every model your API key can reach | transcription checkpoints only |
| Fields | binary field, model, prompt, optional system prompt, path | binary field, model, endpoint, language |
| Answers | whatever the model says about the audio | a transcript |
Analyze Audio takes the audio from the incoming item's binary field, inlines
it as base64 in an input_audio content part, and can use either path — Chat
Completions (the default, which every RexAi provider already reads) or Responses.
The audio format (wav, mp3, ogg, flac, m4a, webm, aac) is taken from
the binary's mime type, falling back to the filename extension and then to mp3.
Transcribe Audio asks for a file and nothing else — no prompt, no system
prompt, and no session header, because the transcription endpoints keep no thread
to chain. Its model list is a short hardcoded one (whisper-1,gpt-4o-transcribe, gpt-4o-mini-transcribe) rather than a filtered view of/v1/models, because those endpoints pick their own backend and a chat model is
not a transcription engine.
Model pickers
Chat, Image Analysis, Analyze Audio and Image Generation all offer the same
list: whatever GET /v1/models returns for your API key. That endpoint already
filters by the key's scopes and visibility, so every entry is a model your key can
actually call — and rexai-auto is in it whenever your key is scoped to it. The
node does not filter further, because it cannot: /v1/models carries no
capability field, so a second guess in the client could only hide a working model.
If you pick a model that cannot read the attachment, RexAi refuses it with a
message naming the missing capability.
One exception: rexai-auto does not appear under Transcribe Audio, for the
reason above.
Development
npm install
npm run build # tsc + copy the icons into dist/
npm run typecheck # tsc --noEmit
npm run verify # static checks on dist/, then the runtime and LangChain checks
tsc only emits .js/.d.ts/.map, so scripts/copy-assets.mjs copies the.svg icons into dist/. Without it, every icon would point at a file that
does not exist and n8n would render a broken image with no error.
npm run verify runs three scripts, because the failures here are all silent ones.
verify-package.mjs reads the built files and checks the things n8n does not
complain about: a node path that does not resolve, an icon that is not beside the
module declaring it, a session header written without a blank guard, a model field
whose picker method is not registered.
verify-runtime.mjs then runs the compiled nodes against a stand-in execution
context and asserts the wiring that only exists at call time — that the Chat Model
really hands the tracer to its model, that the tracer files run data underai_languageModel and under the index addInputData returned, that a tracing
failure cannot break a model call, that the blank-header guard holds, and that the
picker reads /v1/models and falls back to a list the default is in. It touches no
network and never invokes a model.
verify-langchain.mjs closes the gap the other two cannot reach. They call the
tracer's handlers by hand, so they prove the handlers write the right thing — but
not that LangChain ever calls them. The animated border and the in-node output
both depend on LangChain honouring a callback passed to the ChatOpenAI
constructor, and if it did not, every other check would still pass while a
user's canvas showed a dead circle. This script builds a real ChatOpenAI with
the real tracer attached and stubs only the network, so the callbacks have to
fire for real. It covers the success path (start opens the run, end closes it and
writes the generation) and the error path (a failed call must clear the border
rather than leave it spinning), and it includes a control that runs a model with
no callback attached and asserts nothing is recorded — so the harness is proven
able to fail, not just able to pass.
Notes
n8n-workflowand@langchain/coreare peer dependencies — n8n provides
them at runtime, and bundling a second copy is how a node ends up with two
incompatible LangChain cores.@langchain/openaiis a real dependency: the Chat Model reuses LangChain'sChatOpenAIpointed at the RexAi base URL, which is what gives it streaming,
tool calling and the OpenAI wire format for free.
Publishing
The package becomes installable by name as soon as it is on npm.
Community node (install by name — self-hosted only)
The three requirements are already met here: the name starts with n8n-nodes-,n8n-community-node-package is in keywords, and the n8n block lists every node
and credential.
npm login
npm run build
npm publish # unscoped, so it publishes public by default
Then, on any self-hosted instance: Settings → Community Nodes → Install, entern8n-nodes-rexai — or n8n-nodes-rexai@<version> to pin one — accept the risk notice,
and install. Only Owner/Admin users can install community nodes. n8n Cloud does not
allow unverified community nodes at all.
Shipping an update
npm version patch # or minor / major - rewrites package.json and tags a commit
npm run verify # all three scripts must pass before you publish
npm publish --otp=123456
The registry is versioned and append-only: npm publish refuses to overwrite a
version that already exists, so the bump is what makes it a new release. Instances
that already have the package show an Update button under Settings → Community
Nodes. To land on one specific version instead, uninstall and reinstall withn8n-nodes-rexai@<version>.
npm publish returns 403 "Two-factor authentication ... required"
The package is fine; the credential is not. Publishing requires one of:
2FA enabled on the account — and it must be
auth-and-writes, notauth-only, because
publishing is a write. Then pass the code:npm publish --otp=123456. An interactivenpm publishprompts for it itself.A granular access token with "Bypass two-factor authentication" checked — no code per
publish, and the only route that works unattended in CI. Create it on npmjs.com (avatar →
Access Tokens → Generate New Token → Granular Access Token) with Bypass
two-factor authentication checked, Permissions = Read and write (publish and stage),
Select Packages = All Packages, and an expiry. Then set it and publish:npm config set //registry.npmjs.org/:_authToken=<token>
A token minted by npm login is not sufficient on its own — that is exactly what produces
this 403. Check the current state with npm profile get and look at the two-factor auth line.
npm publish exits 0 with PUT 202, but the version never appears
The 202 is npm accepting the upload, not finishing it. Everything below can look
like success while nothing is published:
notice Your package is being processed and may take a few minutes to become available.
http fetch PUT 202 https://registry.npmjs.org/<package> 1787ms
verbose exit 0
Replication normally takes a couple of minutes. If the version is still missing
after ~15, treat it as not published and check, in this order:
Read the registry directly, not through a cached client.
npm viewcan
be served from a CDN edge holding the old document:$u = "https://registry.npmjs.org/<package>?cb=$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" (Invoke-RestMethod $u).versions.PSObject.Properties.NameIf the new version is absent there, it is genuinely not published — whatever
the page shows and whatevernpm viewsays.Check for a rejection email. After the 202, npm can still withhold a
version (its publish pipeline scans uploads). The maintainer gets an email
naming the reason, and the package page shows nothing.Just publish again. The registry is append-only, so this is only safe
because step 1 proved the version does not exist. If it does exist, npm
refuses withcannot publish overand nothing is lost.A 404 on the PUT means the wrong account, not a missing package — npm
returns 404 rather than 403 so it does not leak who owns what. Comparenpm whoamiwithnpm view <package> maintainers. They must match.
A retry immediately after a 404 is the usual fix: the 404 is the first attempt
happening before the new login token is picked up, and the second attempt
succeeds.
The update fails with Error loading package "..." : npm package not found
Almost always transient, and nothing to do with your package. The registry serves a
tarball through a CDN with Cache-Control: public, max-age=300, so for a few minutes afternpm publish that CDN can still be holding a cached 404 for the new version's tarball.
n8n's install and update path runs npm pack <name>@latest (seecommunity-packages.service.ts -> downloadPackage). npm pack fetches the tarball, so a
cached 404 makes it fail with E404; n8n maps that toRESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND ("Package not found in npm") and reports it asUserError('npm package not found'). The registry metadata is fine the whole time, which is
why npm view and the npmjs.com page look correct while n8n insists the package is missing.
Check before doing anything drastic:
npm view n8n-nodes-rexai@latest version # must print the version you just published
npm pack n8n-nodes-rexai@latest --quiet # must print a .tgz filename
If both succeed, the package is fine — wait out the five minutes and press Update again.
If npm pack still fails, the publish genuinely did not land. Bump the version and publish
again; npm will not overwrite a version that already exists, so a re-publish of the same
number is not an option. Republishing the same version also cannot fix it, because the
registry metadata already records it.
Verified node (nodes panel, and Cloud)
Optional, and a much higher bar — submit through the n8n Creator Portal.
Two constraints to plan for:
- From 1 May 2026 a submission must be published from GitHub Actions with an npm
provenance statement, not from a local machine. That needs a public GitHub repo and a.github/workflows/publish.yml(the n8n-nodes-starter workflow). - Verified nodes may have no run-time dependencies. This package currently depends on
@langchain/openaiandform-data, so that route requires bundling them intodist/
first.