n8n-nodes-gemini-token-telemetry
v0.5.0
Published
Google Gemini chat model for n8n that reports the full token usage the default node discards: cached input, billable input, reasoning tokens and cache hit ratio.
Maintainers
Readme
n8n-nodes-gemini-token-telemetry
A drop-in replacement for n8n's Google Gemini Chat Model node that reports the token usage the default node throws away — most importantly how much of your input came from Gemini's context cache, which is billed at a fraction of the normal input rate.
If you run a long system prompt through an n8n AI Agent, cached input is probably the single largest factor in your bill, and today it is invisible.
The problem
Google's API returns ten usage fields per call. Three reach your execution data. Two independent layers drop the rest:
1. LangChain flattens it. @langchain/google-genai keeps the rich data on the message
(generations[0].message.usage_metadata, including input_token_details.cache_read) but
builds llmOutput.tokenUsage with only three fields:
llmOutput: { tokenUsage: {
promptTokens: usageMetadata.input_tokens,
completionTokens: usageMetadata.output_tokens,
totalTokens: usageMetadata.total_tokens,
} }2. n8n deletes the message. N8nLlmTracing.handleLLMEnd runs
pick(g, ['text','generationInfo']) over the generations — dropping message, and with it
usage_metadata. Crucially this happens before tokensUsageParser is called, so a custom
parser alone cannot recover anything.
This is not Gemini-specific. n8n's own Anthropic parser reads only input_tokens /
output_tokens and ignores cache_read_input_tokens, which Anthropic does return.
What this node does
It subclasses the chat model and enriches llmOutput.tokenUsage inside _generate, before
any callback exists — so it does not depend on callback ordering, which is not guaranteed.
A matching tokensUsageParser then copies the extra fields into the persisted output.
No patching of n8n. No patching of LangChain. Only two options n8n already accepts per node.
Fields added to tokenUsage
| Field | Meaning |
|---|---|
| cacheReadTokens | input tokens served from Gemini's context cache |
| inputTokensTotal | total input tokens (cached + billable) |
| inputTokensBillable | inputTokensTotal - cacheReadTokens |
| cacheHitRatio | cacheReadTokens / inputTokensTotal |
| outputTokensVisible | candidatesTokenCount — the text the model actually returned |
| reasoningTokens | thinking tokens (billed as output) |
| reasoningDerived | true when reasoning was derived, not reported |
| outputTokensBillable | outputTokensVisible + reasoningTokens |
| inputTokensOver200k / cacheReadOver200k | long-context tier counts, when present |
| usageSource | gemini-token-telemetry-v1 — proves the value came from this node |
Two deliberate deviations
Reasoning is derived, not reported. Google's thoughtsTokenCount is not mapped by
LangChain into any field — but totalTokenCount includes it, so
reasoning = total - input - visible output. reasoningDerived: true marks that this was
arithmetic, not a provider field.
completionTokens becomes billable output (visible + reasoning), with the original
value preserved in outputTokensVisible. Two reasons: it is the correct number for cost,
and it prevents total data loss — n8n only persists real usage
if (tokenUsage.completionTokens > 0). When Google omits candidatesTokenCount the default
behaviour is to discard the real numbers and silently store a tiktoken estimate instead.
Measured
Against a 19,691-token stable prefix, second call (warm cache):
default node { promptTokens: 19691, completionTokens: 1, totalTokens: 19692 }
this node { promptTokens: 19691, completionTokens: 489, totalTokens: 20180,
cacheReadTokens: 16357, inputTokensTotal: 19691,
inputTokensBillable: 3334, cacheHitRatio: 0.8307,
outputTokensVisible: 1, reasoningTokens: 488,
reasoningDerived: true, outputTokensBillable: 489,
usageSource: "gemini-token-telemetry-v1" }83% of the input was cached and previously invisible. Verified working inside an AI Agent with tool calling, not only in a basic chain.
Cost (optional)
n8n already has a first-class cost slot: its default parser reads
tokenUsage.cost ?? tokenUsage.totalCost and propagates it to the tracing attribute
llm.cost.total. No provider fills it — the only occurrence of totalCost in the n8n codebase
is the line that reads it. This node fills it.
Prices come pre-filled. The package ships a built-in table for the Gemini models, copied from the official pricing page, and each model carries validity windows — so a promotional price switches over on its own date instead of quietly becoming wrong:
| Model (Standard tier) | Input | Output (incl. thinking) | Cache read |
|---|---|---|---|
| gemini-3.7-flash | 0.75 → 1.50 on 2027-01-01 | 3.75 → 7.50 | 0.075 → 0.15 |
| gemini-3.6-flash | 0.75 → 1.50 on 2027-01-01 | 3.75 → 7.50 | 0.075 → 0.15 |
| gemini-3.5-flash | 1.50 | 9.00 | 0.15 |
| gemini-3.5-flash-lite | 0.30 | 2.50 | 0.03 |
Every cost row carries costPricingSource: builtin:<date the table was copied> or
node-parameters when you override. The number always says where it came from and how old
it is — that is the whole point of shipping a table instead of a constant.
Override by setting the three prices in the node's Options (per 1M tokens); yours win. A model that is not in the table and has no prices set emits no cost field at all — an absent number beats a zero that looks like a result.
⚠️ There is no pricing API. GET /v1beta/models returns twelve fields and none of them is a
price (measured: 53 models, zero price fields), and the Cloud Billing catalog rejects API keys
outright. So the table is a copy, and it has a date on it.
⚠️ Standard tier only. Batch is roughly half, Flex and Priority differ. The API does report
serviceTier in usageMetadata, but LangChain drops it before it reaches the node, so it
cannot be detected — Standard is assumed.
⚠️ The cache figure is the read price. Explicit caching also bills storage per hour, which is not derivable from token counts. Implicit caching — what you get by default — has no storage charge.
| Field | Meaning |
|---|---|
| cost | total for the call |
| costInput / costCachedInput / costOutput | the three components |
| costCacheSavings | what the cache saved versus paying full input price |
| costCurrency | a label only — no conversion is performed |
| costPricingSource | node-parameters |
If you leave the cached-input price at 0, cached tokens are charged at the full input
price. That errs upward on purpose, and costCacheSavings then reports 0 rather than
inventing a discount.
Measured on two consecutive calls with the same prompt, at 0.30 / 0.075 / 2.50 USD per 1M:
cold cache cost 0.00712980 cacheReadTokens 0 savings 0
warm cache cost 0.00349698 cacheReadTokens 16357 savings 0.00368033The cache halved the cost of an identical call. That is the number the default node cannot show.
The Model field starts empty — on purpose
n8n's frontend strips any parameter whose value equals the node's declared default before
sending it. With a "helpful" default like models/gemini-3.6-flash, anyone picking that exact
model has their choice silently erased on save. The node keeps working — off the default — and
nobody can answer "which model is running?" by looking at the workflow.
That is not hypothetical: it happened in production here, on the node serving 100% of traffic, and the detector meant to catch it was blind because it matched on the old node type.
With default: "", no legitimate choice can collide with the default, so every choice is stored.
Leaving it empty raises a clear error naming the fix, instead of calling the API with no model
and letting the provider return something confusing.
What's new
0.5.0 — the Model field no longer has a default, so the UI can never erase your choice. 0.4.0 — built-in Gemini price table with validity windows; cost now works out of the box. 0.3.0 — optional cost calculation from user-supplied prices. 0.2.0 — model list is now loaded from the Google API instead of a free-text field. 0.1.1 — added a node icon.
Install
n8n → Settings → Community nodes → Install → n8n-nodes-gemini-token-telemetry
Requires N8N_COMMUNITY_PACKAGES_ENABLED=true. In queue mode, also set
N8N_REINSTALL_MISSING_PACKAGES=true so workers reinstall the package on boot when their
filesystem is ephemeral.
Use the existing Google Gemini(PaLM) API credential. Then point your Agent or Chain at this node instead of the built-in Gemini model node.
Limitations
- Only the non-streaming path (
_generate) is instrumented. Streaming is untested. reasoningTokensis derived; if Google ever stops including thinking intotalTokenCount, it will read zero rather than wrong — but verify before relying on it.- The model list is a plain text field, not the dynamic dropdown of the built-in node.
- Tested against n8n 2.32.7 and
@langchain/google-genai2.1.24.
License
MIT
