n8n-nodes-imoxto
v0.1.6
Published
Is for now just a wrapper of https://www.npmjs.com/package/n8n-nodes-deepseek-reasoner
Readme
A fork of https://www.npmjs.com/package/n8n-nodes-deepseek-reasoner but with cost reporting as i couldnt find a way to get all types of token counts in n8n.
Layout
src/credentials/AscDeepSeekReasonerApi.credentials.ts credential definition
src/nodes/AscDeepSeekReasoner/AscDeepSeekReasoner.node.ts node + ChatDeepSeekReasoner chat model
test/unit/ offline tests (stubbed fetch)
test/live/ tests against the real APIs (DeepSeek and Z.ai)Fallback model
The node takes an optional Fallback Model alongside Model. Leave it empty and nothing changes.
Set it, and once the configured model has used up its whole retry budget (Max Retries in Options), the same
request is sent again with the fallback model — same credential, same Base URL, same body, only model differs.
That includes thinking, reasoning_effort, the bound tools and the replayed reasoning_content on assistant
turns, so a failover mid-conversation still satisfies DeepSeek's reasoning echo. A fallback that is blank, or that
just repeats the primary, is ignored.
Only a model that has exhausted its retries is abandoned, so a transient blip retries on the model you asked for rather than silently switching. If the fallback fails too, its error is the one that surfaces.
Which model gets reported
Whatever answered. The provider's own model field is passed through untouched; when a provider omits it, the node
fills in the model it sent. That value reaches n8n through llmOutput.model_name, the generation's
generationInfo.model and the message's response_metadata.model_name.
So the Output Report URL payload always names a real model, and gains two fields next to the response body:
| field | meaning |
| --- | --- |
| model | the model the provider says served the request |
| requested_model | the model the node asked for on the successful attempt — the fallback, after a failover |
| fallback_used | true when the fallback answered |
Custom Body Fields are merged last, so they still win on a name clash. Only the successful call is reported: a failed attempt returns no usage, so it is not billable and not posted.
On Max Tokens
On a thinking model max_tokens covers the reasoning too. When the budget runs out mid-thought the API returns
finish_reason: "length", an empty content and a reasoning_content that stops partway. The node used to fall
back to that reasoning as the answer, which leaked the raw chain of thought into the output.
A final answer cut off at max_tokens now follows On Max Tokens in Options. Only the debug setting ever
outputs the reasoning:
| setting | result | | --- | --- | | Throw Error | the call fails with a message saying whether it ran out while reasoning or partway through the answer; catch it with the Agent's On Error → Continue (using error output) | | Return Empty | empty answer | | Return Partial Content (default) | whatever answer text arrived before the cut-off, which may be empty or end mid-sentence | | Return Reasoning (Debug) | the cut-off reasoning and any partial answer, raw, under labels — for finding out why it ran out |
The debug output looks like this:
[Hit the Max Tokens limit (8192) before it finished reasoning]
Reasoning:
The user wants 17*23. I should break it down: 17*20 = 340, then
Partial answer:
...Tool-call turns are passed back to the agent unchanged whatever the setting. The reasoning is still kept in history so the next turn echoes it. With the debug setting, a chat memory node stores the debug text as the assistant's reply, so the next turn of that conversation sees it — use it while investigating, not in production.
Scripts
| script | what it does |
| --- | --- |
| npm run build | compile src/ to dist/ and copy node icons |
| npm run build:compare | same, but emits to dist2/ for diffing against the shipped dist/ |
| npm run typecheck | type-check sources and tests without emitting |
| npm test | unit tests (no network) |
| npm run test:live | live tests against DeepSeek and Z.ai |
| npm run test:all | both |
| npm run test:coverage | unit tests with V8 coverage into coverage/ |
Live tests
test/live/ bills real accounts. Every case runs against each provider in
test/live/providers.ts, so both endpoints get identical coverage. Keys go in .env
(see .env.example):
DEEPSEEK_API_KEY=sk-...
Z_API_KEY=...
# optional overrides
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_TEST_MODEL=deepseek-flash
Z_BASE_URL=https://api.z.ai/api/paas/v4
Z_TEST_MODEL=glm-5.3-flashA provider skips itself when its key is unset, and RUN_LIVE_TESTS=0 forces all of them off.
Provider differences
Both endpoints speak the same dialect (thinking: { type } on the request, reasoning_content on the response),
so the node drives them unmodified — only the credential's Base URL changes. Two behaviours differ, and
providers.ts records them as flags the live suite asserts:
| | DeepSeek | Z.ai (glm-5.3-flash) |
| --- | --- | --- |
| Rejects a thinking-mode turn that dropped reasoning_content | yes — the reason this node exists | no, it is tolerated |
| Accepts thinking: {type: 'disabled'} | yes | no — HTTP 400 code 1210, "This model always engages in thinking and cannot be disabled" |
So Enable Thinking = off does not work against glm-5.3-flash: the node states the disable explicitly and the
model rejects it. reasoning_effort accepts low, high and max there; anything else is a 400.
DeepSeek renamed its models
GET /models on DeepSeek now lists deepseek-flash and deepseek-v4-pro. The old deepseek-v4-flash still
works, but as an alias: the request succeeds and the response reports model: "deepseek-flash". The node
passes that through rather than echoing the name it sent, so reporting names the model that was actually billed —
providers.ts pins the alias and the live suite asserts it.
The node's Model parameter still defaults to deepseek-v4-flash, which therefore keeps working. Worth
changing to deepseek-flash the next time the node's defaults are revised.
dist vs dist2
src/ was reconstructed from the JavaScript that used to be committed under dist/.
test/unit/build-parity.test.ts recompiles to dist2/ and asserts the emitted
JavaScript still matches that original line for line, apart from an explicit
allow-list of intended changes. The baseline is read from git at commit 67cdf52,
so it keeps working now that dist/ is built rather than committed.
