Skip to content

architecture(agent-finops): define canonical model-usage and cost attribution contract #49

Closed
mctlhq/mctl-agents
#447

Description

@mashkovd

Parent roadmap

  • mctlhq/.github#48

Implementation owner

Primary implementation is expected in mctlhq/mctl-agents, but the current GitHub integration cannot create issues in that repository, so this cross-repo architecture child lives here until the contract is accepted/decomposed further.

Goal

Define the canonical, backend-neutral model usage and cost attribution contract for mctl agent executions before dashboards or budget enforcement are added.

The contract must make one model invocation attributable to the exact execution/work item/repository/issue/PR/agent stage that caused it, without requiring prompt/completion storage or depending on a specific observability backend.

Scope

Invocation-path inventory

Inventory all currently relevant model invocation paths in mctl-agents, including at minimum:

  • investigator;
  • implementer;
  • shepherd/review-feedback;
  • reviewer agents where mctl can observe/pay usage;
  • any other Temporal/Argo agent worker paths that call a model.

For each path record provider/model, SDK/API entry point, usage fields exposed, cache/reasoning usage, provider-reported cost availability, retry/replay behavior, and current execution/trace correlation.

Canonical ModelUsageRecord

Define a versioned record with fields equivalent to:

id
timestamp
execution_id
workflow_id / run_id
work_item_id?
repository / issue / pr?
agent / devloop_stage
provider / model
usage:
  input_tokens
  output_tokens
  cache_read_tokens?
  cache_write_tokens?
  reasoning_tokens?
provider_reported_cost?
calculated_cost?
pricing_version?
trace_id / span_id?
outcome
retry_attempt / provider_request_id?

Cost semantics

Keep these concepts distinct:

  1. provider_reported_cost — exact value only when the provider/API reports it;
  2. calculated_cost — usage multiplied by a versioned mctl price catalog;
  3. invoice_reconciled_cost — optional future reconciliation with provider billing exports.

A calculated/list-price value must never be presented as exact billing.

OpenTelemetry mapping

Use current OTel GenAI semantic conventions where useful, but define an mctl-owned compatibility mapping/version so changes in external semantic conventions cannot silently corrupt historical analytics.

Idempotency / retries

Define deterministic dedupe semantics for provider retry, Temporal retry/replay, process restart, and telemetry/export reprocessing. The same paid model invocation must not be counted twice.

Deliverables

  • Invocation-path inventory.
  • Versioned ModelUsageRecord contract/schema.
  • Correlation contract covering execution/workflow/work-item/repo/issue/PR/agent/stage.
  • Provider → normalized usage mapping table.
  • Cost semantics and pricing-version rules.
  • Idempotency/dedupe design.
  • OTel compatibility mapping/versioning decision.
  • Repository boundary recommendation for durable storage/query API.

Acceptance criteria

  • Every in-scope model-call path can map to the same usage contract.
  • Input/output/cache/reasoning usage remains separately representable when providers expose it.
  • Provider-reported, calculated and reconciled costs cannot be confused by consumers.
  • Historical calculated cost remains reproducible after price changes.
  • Retry/replay semantics prevent double counting.
  • Prompt/completion payloads are not required for attribution.
  • The contract remains valid if Traceway/Langfuse/Tempo/Phoenix are replaced.

Non-goals

  • Dashboards.
  • Hard budget enforcement.
  • Replacing provider billing/invoices.
  • Selecting the final observability backend.
  • Persisting prompt/completion content for FinOps.

Activity

  1. mashkovd commented on Sep 11, 2026

    @mashkovd
    MemberAuthor

    Decision 2026-09-11 (billing model): DevLoop stays on the Claude subscription OAuth token for now. The contract therefore ships with a subscription-quota mode: SDK-reported cost is recorded as calculated (source = Claude Code price table, pricing version pinned), provider_reported_cost stays null, and quota-window consumption is the billable dimension. Estimates are acceptable for the pilot; a move to API-key billing is not a precondition. invoice_reconciled_cost stays defined but unused until that changes.

  2. mashkovd commented on Sep 22, 2026

    @mashkovd
    MemberAuthor

    Closure evidence

    Delivered as ADR-012 — docs/adr/012-model-usage-cost-attribution-contract.md
    in mctlhq/mctl-agents, merged as ce50386 (PR mctlhq/mctl-agents#447).

    It lives there rather than here because this issue says primary implementation
    is expected in mctl-agents, and that repo already has the ADR convention
    (docs/adr/, 005–011) that usage-ledger implementers read. This issue was
    the cross-repo placeholder "until the contract is accepted".

    Deliverables

    Deliverable Where
    Invocation-path inventory §Invocation-path inventory — all six paths, with entry point, whether each already reads ResultMessage, and model selection
    Versioned ModelUsageRecord §The record — schema_version on the envelope
    Correlation contract §Correlation contract — joins the existing agent_executions spine
    Provider → normalized usage mapping §What the SDK actually exposes + the OTel table
    Cost semantics and pricing-version rules §Cost semantics
    Idempotency/dedupe design §Idempotency and dedupe
    OTel compatibility mapping/versioning §OpenTelemetry mapping, otel_mapping_version
    Repository boundary recommendation §Storage boundary — mctl-api, sibling to agent_executions

    Acceptance criteria

    • Every in-scope path maps to the same contract — testable invariant 1. The
      inventory also records the real cost: three of the six (run_shepherd,
      run_incident_responder, run_mentor) never touch ResultMessage today.
    • Input/output/cache/reasoning separately representable — all four in the
      schema, with reasoning_tokens nullable and absent because SDK 0.2.136
      does not expose it. Absent-vs-zero is a contract rule (invariant 9), so
      "nothing spent" and "nothing measured" stay distinguishable.
    • The three costs cannot be confused — §Cost semantics table plus invariants
      5 and 6. Per your 2026-09-11 subscription decision, provider_reported_cost
      is always null: costUSD is a price table applied to tokens, and putting
      it in the provider field would launder an estimate into a billing claim.
    • Historical calculated cost reproducible — invariant 7: a stored
      calculated_cost is never recomputed; a price change makes new records under
      a new pricing_version.
    • Retry/replay prevent double counting — deterministic
      sha256(session_id|result_uuid|model_key) + idempotent upsert, invariants 3
      and 4. The contract keeps the distinction explicit: a retry that re-runs the
      model
      is a new invocation with a real new charge and must not be deduped.
    • Prompt/completion not required — invariant 8: the record has no field of
      that shape.
    • Valid if the backend is replaced — the OTel mapping is mctl-owned and
      versioned, and cost is deliberately not a span attribute: spans are
      sampled, a ledger must not be, and a figure under-counted by exactly the
      sampling rate would still look plausible.

    Two findings worth carrying forward

    1. Nothing is measured today. grep -rn "total_cost_usd\|model_usage" --include="*.py" orchestrator/ returns no matches. usage-ledger starts
      from zero instrumentation, not from data in the wrong shape.
    2. The record grain is (run, model), not run. model_usage is keyed by
      model, and a run legitimately spans a main loop plus a cheap-profile
      sub-agent — a per-run record would merge them irrecoverably.

    The SDK surface was read from the pinned wheel (sdist from uv.lock, SHA-256
    checked against the lock) rather than the copy installed locally, which is
    0.1.76 and has a different ResultMessage. Reading the installed one would
    have documented a contract for a version the repo does not use.

    usage-ledger is now unblocked.

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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions