Story-1440 · pramaan-docs ADR-124 · Agents lane

Atlas emits aggregatable GenAI token and cost metrics

Branch feature/1440-ag-genai-token-cost · Story: pramaan-io/pramaan-spec#1440 · Parent: pramaan-io/pramaan-spec#1400

Why this exists

Span attributes make one run explainable. Metrics make spend and capacity visible across runs. Story-1440 adds the missing metrics layer for Atlas model usage without turning tenant, run, matter, user, trace, or span identifiers into metric dimensions.

The mental model: traces answer "what happened to this run"; metrics answer "what is the fleet doing by model, provider, agent, and environment". Those are different jobs, so their labels are different.

What changed

runtime.handle_request()
  final input/output token totals
  final computed USD cost
          |
          v
observability.record_genai_token_usage_and_cost()
  gen_ai.client.token.usage histogram
  pramaan.gen_ai.cost counter
          |
          v
dedicated Logfire OTLP metrics exporter
  https://logfire-us.pydantic.dev/v1/metrics
MetricTypeUnitAllowed labels
gen_ai.client.token.usagehistogram{token}gen_ai.token.type, model_id, provider, agent.key, deployment.environment.name
pramaan.gen_ai.costcounterUSDmodel_id, provider, agent.key, deployment.environment.name
Forbidden metric dimensions remain forbidden: firm_id, run_id, matter_id, user_id, trace_id, and span_id.

Semconv split

Standard OTel keysPRAMAAN product keys
gen_ai.client.token.usage, gen_ai.token.type, gen_ai.agent.name=Atlas, service.name=pramaan-agents-atlas, deployment.environment.nameagent.key=atlas, model_id. run_id, firm_id, and form_id stay trace/log context, not metric labels.

The runtime pins OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental before Pydantic AI instrumentation.

Daily cost trend query

-- Logfire / OTLP metrics view
SELECT
  date_trunc('day', timestamp) AS day,
  attributes['deployment.environment.name'] AS env,
  attributes['agent.key'] AS agent_key,
  attributes['provider'] AS provider,
  attributes['model_id'] AS model_id,
  sum(value) AS daily_cost_usd
FROM metrics
WHERE metric_name = 'pramaan.gen_ai.cost'
GROUP BY 1, 2, 3, 4, 5
ORDER BY day DESC, daily_cost_usd DESC;

Proof

The unit tests assert the metric registry contract directly, the Logfire export path, the semconv opt-in, and the runtime call from final token totals. They also plant a forbidden run_id label and prove the metric-label guard rejects it.

Verification target: ruff check tests/ scripts/ agents/atlas, Atlas focused pytest, root guard tests, remote metric registry guard for the merged pramaan-docs registry, and git diff --check.