Skip to content

Token Usage Metadata Exposure

Overview

AIAtlasNexus.identify_risks_from_usecases() exposes LLM token usage metrics via an optional return_metadata parameter.

At the detector level this is a decorator rather than a flag: wrap any detector in RiskDetectorWithMetadata to get the same information.

from ai_atlas_nexus.blocks.risk_detector import (
    GenericRiskDetector,
    RiskDetectorWithMetadata,
)

detector = RiskDetectorWithMetadata(GenericRiskDetector(...))
result = detector.detect(["my AI system description"])

This enables monitoring and cost analysis of risk identification operations.

Usage

Basic API (Backward Compatible)

By default, the API returns a list of risks:

risks = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=["my AI system description"],
    inference_engine=inference_engine,
    taxonomy="ibm-risk-atlas",
)
# risks: List[List[Risk]]
# Example: [[Risk(...), Risk(...)], ...]

With Token Metadata

Pass return_metadata=True to receive a wrapped response with token usage, reported both aggregated over the run and broken down per usecase:

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=["my AI system description"],
    inference_engine=inference_engine,
    taxonomy="ibm-risk-atlas",
    return_metadata=True,
)
# result: DetectionResult[List[List[Risk]]]

# Access the risks data
risks = result.data
print(f"Identified risks: {[r.name for r in risks[0]]}")

# Access token usage metrics
print(f"Input tokens: {result.metadata.token_usage.input_tokens}")
print(f"Output tokens: {result.metadata.token_usage.output_tokens}")
print(f"Total tokens: {result.metadata.token_usage.total_tokens}")

# Model and inference info
print(f"Model: {result.metadata.model}")
print(f"Engine: {result.metadata.inference_engine}")
print(f"Number of LLM calls: {result.metadata.num_calls}")

# Reproducibility and prediction details
print(f"Seed: {result.metadata.seed}")  # None if calls used different seeds
print(f"Stop reasons: {result.metadata.stop_reason_summary}")  # e.g., {"eos": 99}
print(f"Has thinking: {result.metadata.has_thinking}")  # True if any call had thinking

Per-Usecase Breakdown

metadata.per_usecase has one entry per usecase, in the order the usecases were passed in and positionally aligned with result.data:

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=["customer support chatbot", "loan approval model"],
    inference_engine=inference_engine,
    return_metadata=True,
)

for usecase, risks, usage in zip(usecases, result.data, result.metadata.per_usecase):
    print(f"{usecase}: {len(risks)} risks, {usage.token_usage.total_tokens} tokens "
          f"across {usage.num_calls} call(s)")

The per-usecase totals sum to the aggregate, so either view can be used without double counting. An unreported count is None rather than 0, so default it before doing arithmetic:

assert sum(u.token_usage.input_tokens or 0 for u in result.metadata.per_usecase) == \
    (result.metadata.token_usage.input_tokens or 0)

How many calls each entry covers depends on the mode. With batch_inference=True (the default) it is one call per usecase. With batch_inference=False or use_dspy_prompt=True it is one call per risk per usecase, including the calls that identified nothing — those still cost tokens.

Stop reasons, seeds and has_thinking are also reported per usecase. Note that seed follows the same rule at both levels: it is only set if every call in that scope used the same seed, so a usecase can report a seed while the run as a whole reports None.

With identify_risks_and_actions_from_usecases

The companion method also supports token metadata. It returns one entry per usecase under per_usecase, each with that usecase's own token_usage, and the run totals at the top level:

result = ai_atlas_nexus.identify_risks_and_actions_from_usecases(
    usecases=["a support chatbot", "a loan approval model"],
    inference_engine=inference_engine,
    taxonomy="ibm-risk-atlas",
    return_metadata=True,
)

print(f"Total tokens used: {result['token_usage']['total_tokens']}")

for entry in result["per_usecase"]:
    usage = entry["token_usage"]
    print(f"{entry['usecase']}: {len(entry['risks'])} risks, "
          f"{usage['total_tokens']} tokens across {usage['num_calls']} call(s)")

Without return_metadata=True no token_usage key is present, at either level.

Response Types

DetectionResult[T]

Wraps detection results with metadata:

@dataclass(kw_only=True)
class DetectionResult(Generic[T]):
    data: T  # List[List[Risk]], or List[List[RiskWithExplanation]] if explained
    metadata: InferenceMetadata

InferenceMetadata

Contains inference information for the run, aggregated and per usecase:

@dataclass(kw_only=True)
class InferenceMetadata:
    token_usage: TokenUsage           # Tokens aggregated over the whole run
    inference_engine: str              # Engine type (e.g., "WML")
    model: str                         # Model name/path
    num_calls: int                     # Total LLM calls made
    seed: Optional[int]                # Seed used (None if calls used different seeds)
    stop_reason_summary: Dict[str, int]  # Count of each stop reason (e.g., {"eos": 5})
    has_thinking: bool                 # Whether any call had thinking enabled
    per_usecase: List[UsecaseInferenceMetadata]  # One entry per usecase, in input order

UsecaseInferenceMetadata

The same figures for a single usecase. The engine and model are not repeated, since they are the same for every usecase in a run:

@dataclass(kw_only=True)
class UsecaseInferenceMetadata:
    token_usage: TokenUsage            # Tokens for this usecase only
    num_calls: int                     # LLM calls made for this usecase
    seed: Optional[int]                # Seed used (None if this usecase's calls differed)
    stop_reason_summary: Dict[str, int]  # Count of each stop reason for this usecase
    has_thinking: bool                 # Whether any of this usecase's calls had thinking

TokenUsage

Token metrics from the LLM:

@dataclass(kw_only=True)
class TokenUsage:
    input_tokens: Optional[int]        # Tokens in the prompt
    output_tokens: Optional[int]       # Tokens in the response
    total_tokens: Optional[int]        # Sum of input + output

Use Cases

Cost Tracking

Monitor token consumption for cost analysis:

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=usecases,
    inference_engine=inference_engine,
    return_metadata=True,
)

cost_per_1k_tokens = 0.001  # Example: $0.001 per 1000 tokens
total_tokens = result.metadata.token_usage.total_tokens
if total_tokens is None:
    print("This engine did not report token counts, so cost is unknown")
else:
    print(f"Cost of this operation: ${(total_tokens / 1000) * cost_per_1k_tokens:.4f}")

Or attribute cost to the usecase that incurred it, for chargeback or for finding which usecases are expensive to analyze:

for usecase, usage in zip(usecases, result.metadata.per_usecase):
    cost = ((usage.token_usage.total_tokens or 0) / 1000) * cost_per_1k_tokens
    print(f"{usecase}: ${cost:.4f}")

Performance Analysis

Understand resource consumption patterns:

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=usecases,
    inference_engine=inference_engine,
    return_metadata=True,
)

total_tokens = result.metadata.token_usage.total_tokens
if total_tokens is None or result.metadata.num_calls == 0:
    print("No token counts to average")
else:
    print(f"Average tokens per LLM call: {total_tokens / result.metadata.num_calls:.0f}")

Reproducibility Tracking

Verify if runs are reproducible (same seed used):

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=usecases,
    inference_engine=inference_engine,
    return_metadata=True,
)

if result.metadata.seed is not None:
    print(f"Run is reproducible with seed={result.metadata.seed}")
else:
    print("Run used different seeds, not reproducible")

Stop Reason Analysis

Understand why the model stopped generating:

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=usecases,
    inference_engine=inference_engine,
    return_metadata=True,
)

print(f"Stop reasons: {result.metadata.stop_reason_summary}")
# Example output: {"eos": 95, "max_tokens": 5}
# "eos" = natural end of sequence
# "max_tokens" = hit token limit

Logging and Monitoring

Track usage over time:

result = ai_atlas_nexus.identify_risks_from_usecases(
    usecases=usecases,
    inference_engine=inference_engine,
    return_metadata=True,
)

logger.info(
    "Risk identification completed",
    extra={
        "model": result.metadata.model,
        "engine": result.metadata.inference_engine,
        "input_tokens": result.metadata.token_usage.input_tokens,
        "output_tokens": result.metadata.token_usage.output_tokens,
        "total_tokens": result.metadata.token_usage.total_tokens,
        "num_calls": result.metadata.num_calls,
        "seed": result.metadata.seed,
        "stop_reasons": result.metadata.stop_reason_summary,
        "has_thinking": result.metadata.has_thinking,
    }
)

Supported Engines

Token metrics are aggregated across all supported inference engines