Summary
For Amazon Titan embedding responses (e.g. amazon.titan-embed-text-v2:0), the Bedrock instrumentation emits gen_ai.usage.total_tokens = NaN. Embedding responses contain inputTextTokenCount but no output-token field, and the total is computed as input + undefined, which in JS silently evaluates to NaN. The attribute is then serialized as the string "NaN" on the exported span.
Environment
@traceloop/instrumentation-bedrock via @traceloop/node-server-sdk
- Node.js 22,
InvokeModel with amazon.titan-embed-text-v2:0
- Bug is present on
main (1faf69d)
Observed span attributes
gen_ai.operation.name: embeddings
gen_ai.request.model: titan-embed-text-v2:0
gen_ai.usage.input_tokens: 117
gen_ai.usage.total_tokens: "NaN" <-- string, poisons the attribute
(gen_ai.usage.output_tokens absent)
In our production telemetry, 100% of spans carrying total_tokens from this code path have the value "NaN" (152 spans over 7 days).
Root cause
Two sites guard only the input side before adding input + output:
-
Non-streaming Titan path — titanOutputTokens comes from response["results"]?.[0]?.["tokenCount"], which doesn't exist on embedding responses:
|
...(titanInputTokens != null |
|
? { |
|
[ATTR_GEN_AI_USAGE_INPUT_TOKENS]: titanInputTokens, |
|
[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: titanOutputTokens, |
|
[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS]: |
|
titanInputTokens + titanOutputTokens, |
|
} |
-
Streaming path ("Titan includes token counts on the final chunk") — same pattern with response["totalOutputTextTokenCount"]:
|
...(response["inputTextTokenCount"] != null |
|
? { |
|
[ATTR_GEN_AI_USAGE_INPUT_TOKENS]: |
|
response["inputTextTokenCount"], |
|
[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: |
|
response["totalOutputTextTokenCount"], |
|
[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS]: |
|
response["inputTextTokenCount"] + |
|
response["totalOutputTextTokenCount"], |
|
} |
The existing tests only cover the happy path where both counts exist (amazon.test.ts asserts input + output), so the embedding case is never exercised.
Impact
Beyond the value being wrong, emitting the string "NaN" has a nasty downstream effect: OTel backends that infer attribute types see a string and lock gen_ai.usage.total_tokens to STRING for the dataset, which breaks numeric aggregations (SUM/AVG/percentiles) over the attribute even for spans where the value would have been numeric.
Suggested fix
Only emit GEN_AI_USAGE_TOTAL_TOKENS when both operands are numbers, and treat missing output as 0 for operations that legitimately have no output tokens (embeddings are billed on input only):
...(titanInputTokens != null
? {
[ATTR_GEN_AI_USAGE_INPUT_TOKENS]: titanInputTokens,
...(titanOutputTokens != null
? { [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: titanOutputTokens }
: {}),
[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS]:
titanInputTokens + (titanOutputTokens ?? 0),
}
: {}),
Same guard for the streaming site. Happy to open a PR if that approach sounds right.
Summary
For Amazon Titan embedding responses (e.g.
amazon.titan-embed-text-v2:0), the Bedrock instrumentation emitsgen_ai.usage.total_tokens = NaN. Embedding responses containinputTextTokenCountbut no output-token field, and the total is computed asinput + undefined, which in JS silently evaluates toNaN. The attribute is then serialized as the string"NaN"on the exported span.Environment
@traceloop/instrumentation-bedrockvia@traceloop/node-server-sdkInvokeModelwithamazon.titan-embed-text-v2:0main(1faf69d)Observed span attributes
In our production telemetry, 100% of spans carrying
total_tokensfrom this code path have the value"NaN"(152 spans over 7 days).Root cause
Two sites guard only the input side before adding input + output:
Non-streaming Titan path —
titanOutputTokenscomes fromresponse["results"]?.[0]?.["tokenCount"], which doesn't exist on embedding responses:openllmetry-js/packages/instrumentation-bedrock/src/instrumentation.ts
Lines 642 to 648 in 1faf69d
Streaming path ("Titan includes token counts on the final chunk") — same pattern with
response["totalOutputTextTokenCount"]:openllmetry-js/packages/instrumentation-bedrock/src/instrumentation.ts
Lines 603 to 612 in 1faf69d
The existing tests only cover the happy path where both counts exist (
amazon.test.tsassertsinput + output), so the embedding case is never exercised.Impact
Beyond the value being wrong, emitting the string
"NaN"has a nasty downstream effect: OTel backends that infer attribute types see a string and lockgen_ai.usage.total_tokensto STRING for the dataset, which breaks numeric aggregations (SUM/AVG/percentiles) over the attribute even for spans where the value would have been numeric.Suggested fix
Only emit
GEN_AI_USAGE_TOTAL_TOKENSwhen both operands are numbers, and treat missing output as0for operations that legitimately have no output tokens (embeddings are billed on input only):Same guard for the streaming site. Happy to open a PR if that approach sounds right.