Skip to content

fix(instrumentation-bedrock): gen_ai.usage.total_tokens is NaN (string) for Titan embedding responses #1031

Description

@BrontoStephen

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:

  1. 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,
    }

  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions