CostUsageService¶
Target
This page describes intended behaviour or structure. No implementation is claimed unless a section is explicitly marked Current implementation or Evidence.
| Bounded context | BC-05 Usage Accounting |
| Platform / plane | Control Plane / Control |
| Deployment granularity | module of host · host ControlPlaneApi |
| Status | target · ConnectSoft.Factory.Usage |
| Built from | connectsoft-microservice · libraries: connectsoft-extensions-saas-abstractions, connectsoft-extensions-saas-asp-net-core, connectsoft-extensions-saas-metering, connectsoft-extensions-messaging-model, connectsoft-extensions-persistence-model-nhibernate |
| Decisions | ADR-0020, ADR-0014 |
| Traceability | FR-09 FUX-08 NFR-08 NFR-09 NFR-12 XL-12 |
Purpose and responsibilities¶
Every run spends money before anyone knows exactly how much. CostUsageService makes that spend governable: it holds budgets per project and tenant, reserves an estimate before Run Control dispatches the first step, records every metered quantity from model and tool receipts with the pricing revision in force, settles the reservation when the run ends, and reconciles estimated or uncertain records against provider statements later. Its usage states (estimated, reserved, observed, reconciled, uncertain) are shown to users as such (FUX-08); nothing is ever silently zeroed.
It is the source for FinOps views in Observability (PF-13) and for the Factory's own meters in FactoryCommercialService (BC-34). It absorbs the June-2026 rows QuotaService (quota denial is a reservation outcome) and CostTelemetryService (telemetry is a projection of these records, not a second source). Entitlement definitions (what an edition allows) come from BC-33; this service enforces amounts.
Context and boundaries¶
flowchart LR
S["CostUsageService"]
RunControlService["RunControlService"] -->|grpc| S
ModelRouterService["ModelRouterService"] -->|grpc| S
ToolAdapterService["ToolAdapterService"] -->|grpc| S
StudioBff["StudioBff"] -->|sync-http| S
S -->|library| ext_saas_templates["ext-saas-templates"]
S -->|event| FactoryCommercialService["FactoryCommercialService"]
S -->|grpc| RunControlService["RunControlService"]
S -. "BudgetReserved" .-> E_BudgetReserved(("event"))
S -. "BudgetReservationDenied" .-> E_BudgetReservationDenied(("event"))
S -. "ReservationSettled" .-> E_ReservationSettled(("event"))
S -. "UsageRecorded" .-> E_UsageRecorded(("event"))
S -. "UsageReconciled" .-> E_UsageReconciled(("event"))
S -. "BudgetUpdated" .-> E_BudgetUpdated(("event"))
The spend approval rule (D21): a run whose estimate exceeds the budget's approval threshold is reserved only after a Reviewer or Workspace admin approves the exact estimate through Run Control. The platform threshold is set by Platform admin policy; a project may only lower it.
Domain model¶
Budget — A spending limit for a project or tenant with the spend-approval threshold and the current committed amount. (catalog entry)
| Field | Type | Required | Description |
|---|---|---|---|
budgetId |
guid |
yes | |
tenantId |
string |
yes | |
projectId |
guid |
no | Null for a tenant-wide budget. |
period |
string |
yes | monthly |
limitUsd |
decimal |
yes | |
committedUsd |
decimal |
yes | Reserved plus settled in the period. |
approvalThresholdUsd |
decimal |
yes | Predicted run cost above which approval is required (D21); platform ceiling, project may lower. |
version |
long |
yes |
Invariants:
- committedUsd never exceeds limitUsd; a reservation that would exceed it is Denied.
- The approval threshold of a project budget is never higher than the platform threshold.
Events: BudgetUpdated · Tables: Budgets · Repository: BudgetRepository
Reservation — An amount held for a run before dispatch and settled from observed usage afterwards. (catalog entry)
| Field | Type | Required | Description |
|---|---|---|---|
reservationId |
guid |
yes | |
tenantId |
string |
yes | |
projectId |
guid |
yes | |
runId |
guid |
yes | |
budgetId |
guid |
yes | |
estimatedUsd |
decimal |
yes | |
settledUsd |
decimal |
no | |
state |
enum:ReservationState |
yes | |
approvalId |
guid |
no | |
expiresAt |
datetime |
yes |
Invariants:
- One active reservation per run; settlement is idempotent on the set of receipts settled.
- A reservation above the threshold is created in Reserved only after the spend approval is granted.
stateDiagram-v2
Reserved --> Settled : RunTerminal
Reserved --> Released : RunCancelledBeforeDispatch
Reserved --> Expired : ExpiryWithoutRun
Events: BudgetReserved, BudgetReservationDenied, ReservationSettled · Tables: Reservations · Repository: ReservationRepository
UsageRecord — One metered quantity attributed to tenant, project, run and attempt, priced with a pricing revision and carrying a reconciliation status. (catalog entry)
| Field | Type | Required | Description |
|---|---|---|---|
usageId |
guid |
yes | |
tenantId |
string |
yes | |
projectId |
guid |
yes | |
runId |
guid |
no | |
attemptId |
guid |
no | |
meter |
enum:Meter |
yes | |
dimensions |
json |
yes | profile, provider, model, tool, pool, store, product, sloTier |
quantity |
decimal |
yes | |
status |
enum:UsageStatus |
yes | |
pricingRevision |
string |
yes | |
estimatedCostUsd |
decimal |
no | |
confirmedCostUsd |
decimal |
no | |
sourceReceipt |
string |
no | |
occurredAt |
datetime |
yes |
Invariants:
- A record is created from a receipt exactly once (sourceReceipt unique per meter).
- Uncertain usage is never zeroed; it is reconciled by a later receipt or by a provider statement.
Events: UsageRecorded, UsageReconciled · Tables: UsageRecords, PricingRevisions · Repository: UsageRecordRepository
erDiagram
Budget {
guid budgetId PK
string tenantId
guid projectId
string period
decimal limitUsd
decimal committedUsd
decimal approvalThresholdUsd
long version
}
Reservation {
guid reservationId PK
string tenantId
guid projectId
guid runId
guid budgetId
decimal estimatedUsd
decimal settledUsd
enum_ReservationState state
guid approvalId
datetime expiresAt
}
UsageRecord {
guid usageId PK
string tenantId
guid projectId
guid runId
guid attemptId
enum_Meter meter
json dimensions
decimal quantity
enum_UsageStatus status
string pricingRevision
decimal estimatedCostUsd
decimal confirmedCostUsd
string sourceReceipt
datetime occurredAt
}
PricingRevision {
string revision
enum_Meter meter
string dimensionKey
decimal unitPriceUsd
datetime validFrom
datetime validTo
}
UsageRecord ||--o{ PricingRevision : contains
stateDiagram-v2
[*] --> Reserved: Reserve within budget
[*] --> Denied: Reserve exceeds budget
Reserved --> Settled: run terminal
Reserved --> Released: cancelled before dispatch
Reserved --> Expired: no dispatch before expiry
Settled --> [*]
Released --> [*]
Expired --> [*]
Denied --> [*]
Usage records are facts, never recomputed: a receipt creates exactly one record per meter (unique on receipt id), reconciliation adds the confirmed cost and status, and pricing revisions are versioned so that a past record always prices with the revision valid when it occurred.
Entities and value objects¶
PricingRevision (entity, in UsageRecord) — Versioned price list per meter and dimension.
| Field | Type | Required | Description |
|---|---|---|---|
revision |
string |
yes | |
meter |
enum:Meter |
yes | |
dimensionKey |
string |
yes | |
unitPriceUsd |
decimal |
yes | |
validFrom |
datetime |
yes | |
validTo |
datetime |
no |
Data model and storage¶
| Store | Role | Engine / profile | Tables |
|---|---|---|---|
| Control plane relational store | primary | azure-sql / nhibernate-sqlserver | Budgets, Reservations, UsageRecords, PricingRevisions |
| Table | Owner | Columns | Indexes |
|---|---|---|---|
dbo.Budgets |
Budget | 8 | 1 |
dbo.Reservations |
Reservation | 10 | 1 |
dbo.UsageRecords |
UsageRecord | 14 | 2 |
dbo.PricingRevisions |
PricingRevision | 6 | 0 |
Full DDL: appendix.
UsageRecords is retained seven years as commercial evidence (D31) and is the largest table of the Control plane; it is partitioned by tenant and month. PricingRevisions is platform-wide (not tenant-scoped).
APIs¶
| Method | Path | Operation | Scope | Side effects | Idempotency | Gateway | Summary |
|---|---|---|---|---|---|---|---|
GET |
/factory/projects/{projectId}/usage |
GetProjectUsage | factory.usage.read |
read | natural | GW-03 | Usage by meter and status (estimated, reserved, observed, reconciled, uncertain) for a period. |
GET |
/factory/runs/{runId}/usage |
GetRunUsage | factory.usage.read |
read | natural | GW-03 | Usage records and the reservation of one run. |
PUT |
/factory/projects/{projectId}/budget |
SetProjectBudget | factory.usage.admin |
shared-write | natural | GW-03 | Set the project budget and a threshold not above the platform's. |
| gRPC service | Method | Streaming | Visibility | Side effects | Summary |
|---|---|---|---|---|---|
Usage |
Reserve |
none | internal | spend | Reserve an estimated amount for a run; returns Reserved, Denied or approval-required. |
Usage |
RecordUsage |
none | internal | shared-write | Record metered usage from a model or tool receipt (idempotent on receipt id). |
Messages¶
| Event | Topic | Scope / v | Aggregate | Consumers | Summary |
|---|---|---|---|---|---|
| BudgetReserved | usage-accounting.reservations.v1.budget-reserved |
integration / 1 | Reservation | RunControlService, StudioBff | A run has budget and may be dispatched. |
| BudgetReservationDenied | usage-accounting.reservations.v1.budget-reservation-denied |
integration / 1 | Reservation | RunControlService, StudioBff | No budget could be reserved; the run fails as policy-denied. |
| ReservationSettled | usage-accounting.reservations.v1.reservation-settled |
integration / 1 | Reservation | FactoryCommercialService, StudioBff, DashboardService | Observed usage settled against the reservation. |
| UsageRecorded | usage-accounting.usage.v1.usage-recorded |
integration / 1 | UsageRecord | FactoryCommercialService, DashboardService | A metered quantity was recorded. |
| UsageReconciled | usage-accounting.usage.v1.usage-reconciled |
integration / 1 | UsageRecord | FactoryCommercialService, DashboardService | An estimated or uncertain record was confirmed. |
| BudgetUpdated | usage-accounting.budgets.v1.budget-updated |
integration / 1 | Budget | StudioBff, AuditService | A budget limit or threshold changed. |
BudgetReserved payload:
| Field | Type | Required | Description |
|---|---|---|---|
reservationId |
guid |
yes | |
runId |
guid |
yes | |
tenantId |
string |
yes | |
estimatedUsd |
decimal |
yes |
BudgetReservationDenied payload:
| Field | Type | Required | Description |
|---|---|---|---|
runId |
guid |
yes | |
tenantId |
string |
yes | |
reason |
string |
yes |
ReservationSettled payload:
| Field | Type | Required | Description |
|---|---|---|---|
reservationId |
guid |
yes | |
runId |
guid |
yes | |
tenantId |
string |
yes | |
settledUsd |
decimal |
yes | |
uncertain |
bool |
yes |
UsageRecorded payload:
| Field | Type | Required | Description |
|---|---|---|---|
usageId |
guid |
yes | |
tenantId |
string |
yes | |
projectId |
guid |
yes | |
meter |
enum:Meter |
yes | |
quantity |
decimal |
yes | |
status |
enum:UsageStatus |
yes |
UsageReconciled payload:
| Field | Type | Required | Description |
|---|---|---|---|
usageId |
guid |
yes | |
tenantId |
string |
yes | |
confirmedCostUsd |
decimal |
yes |
BudgetUpdated payload:
| Field | Type | Required | Description |
|---|---|---|---|
budgetId |
guid |
yes | |
tenantId |
string |
yes | |
projectId |
guid |
no | |
limitUsd |
decimal |
yes |
| Command | Handled by | Result events | Summary |
|---|---|---|---|
Reserve |
Reservation | BudgetReserved, BudgetReservationDenied | |
RecordUsage |
UsageRecord | UsageRecorded |
| Event | Producer | Handler | Purpose |
|---|---|---|---|
| RunAccepted | RunControlService | ReserveForRun |
Estimate and reserve budget for an accepted run. |
| RunCompleted | RunControlService | SettleReservation |
Settle observed usage. |
| RunFailed | RunControlService | SettleReservation |
Settle observed usage of a failed run. |
| RunCancelled | RunControlService | SettleOrRelease |
Release or settle after cancellation. |
| AttemptOutcomeRecorded | RunControlService | RecordAttemptUsage |
Record usage from the attempt receipt. |
Workers and jobs¶
| Worker | Trigger | Output events | Retry | Idempotency key | Summary |
|---|---|---|---|---|---|
| ReconciliationWorker | schedule 0 * * * * |
UsageReconciled | exponential | usageId + statementRef |
Confirms estimated and uncertain usage from provider statements and late receipts; emits UsageReconciled. |
| ReservationExpiryWorker | schedule */15 * * * * |
ReservationSettled | fixed | reservationId + expiresAt |
Expires reservations whose run never dispatched and releases committed amounts. |
Actors and grains¶
None modelled
No grains are modelled for this item yet.
Sagas and long-running flows¶
None modelled
No sagas are modelled for this item yet.
sequenceDiagram
participant RC as RunControlService
participant U as CostUsageService
participant MR as ModelRouterService
participant TA as ToolAdapterService
RC-)U: RunAccepted with limits
U->>U: estimate from profile and workflow, compare with budget and threshold
alt within budget and under threshold
U-)RC: BudgetReserved
else over threshold
U->>RC: RequestApproval class spend bound to the estimate
RC-)U: ApprovalDecided
U-)RC: BudgetReserved or BudgetReservationDenied
else over budget
U-)RC: BudgetReservationDenied
end
MR->>U: RecordUsage from model receipt
TA->>U: RecordUsage from tool receipt
RC-)U: RunCompleted
U->>U: settle reservation, mark unknown receipts Uncertain
U-)RC: ReservationSettled
Options and configuration¶
| Section | Key | Type | Default | Reloadable | Secret | Summary |
|---|---|---|---|---|---|---|
Usage:Thresholds |
PlatformApprovalThresholdUsd |
decimal | 10 |
yes | no | Platform spend threshold above which a run needs approval (D21; original §52 seed). |
Usage:Reservations |
ExpiryMinutes |
int | 60 |
yes | no | Reservation lifetime without dispatch. |
Metrics and observability¶
| Metric | Instrument | Unit | Dimensions | Summary |
|---|---|---|---|---|
connectsoft.factory.usage.cost_usd |
counter | USD | tenant, project, meter, status | Cost by meter and status. |
connectsoft.factory.usage.uncertain_records |
updown | {record} | tenant | Records awaiting reconciliation. |
connectsoft.factory.usage.reservations_denied |
counter | {reservation} | tenant, reason | Denied reservations. |
Security¶
| Authentication | oauth2-jwt |
| Scopes | factory.usage.read (Read usage and budgets.); factory.usage.admin (Set budgets and thresholds.) |
| Roles | Requester, Operator, WorkspaceAdmin, PlatformAdmin, Observer |
| Tenant isolation | row-filter |
| Data classification | internal |
| Max side-effect class | spend |
| Secrets | — |
Dependencies¶
Outbound
| Target | Kind | Purpose | Failure mode | Required |
|---|---|---|---|---|
| ext-saas-templates | library | Metering template as the meter store. | fail-fast | yes |
| FactoryCommercialService | event | Feed invoices and edition quotas. | queue | no |
| RunControlService | grpc | Request a spend approval when the estimate exceeds the threshold. | fail-fast | yes |
Inbound
| Target | Kind | Purpose | Failure mode | Required |
|---|---|---|---|---|
| RunControlService | grpc | Reserve and RecordUsage. | — | yes |
| ModelRouterService | grpc | RecordUsage from model receipts. | — | yes |
| ToolAdapterService | grpc | RecordUsage from tool receipts. | — | yes |
| StudioBff | sync-http | Usage views and budget administration. | — | yes |
Failure and recovery¶
| Scenario | Detection | Recovery | Data impact | RTO (min) |
|---|---|---|---|---|
| Provider returns no usage in a receipt | usage status unknown on the receipt | record created as Uncertain; reconciliation worker confirms from statements | cost shown as uncertain until reconciled | 1440 |
| Budget store conflict under concurrent reservations | version mismatch | retry the reservation; limit never exceeded | none | 0 |
NFR targets¶
| Target | Class value (pilot-interactive) | Override |
|---|---|---|
| latency_p95_ms | 2000 | — |
| availability_pct | 99.5 | — |
| throughput_rps | 50 | — |
| rpo_minutes | 15 | — |
| rto_minutes | 240 | — |
| cost_class | low | — |
Deployment¶
| Granularity | module of host · host ControlPlaneApi |
| Topologies | saas-default, hybrid, on-prem |
| Runtime | —; replicas —–— |
| Ports | — |
Current implementation¶
Nothing exists. The SaaS Metering template exists and is the meter store this service extends (ecosystem composition); MicrosoftExtensionsAIOptions carries no usage receipt.
Later capabilities¶
Predictive budget alerts and anomaly detection (from the former roadmap); per-step cost attribution in the Studio run view; chargeback exports; provider statement import automation per provider.
Decisions and traceability¶
| Family | Ids |
|---|---|
| FR | FR-09 |
| FUX | FUX-08 |
| NFR | NFR-08, NFR-09, NFR-12 |
| XL | XL-12 |