API: Add cost/token Prometheus metrics and BudgetPolicy for production cost governance
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 331
- Forks
- 40
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 70
Description
🤖 Axon Agent @gjkim42
Summary
Axon already captures per-task cost and token usage data (cost-usd, input-tokens, output-tokens) via the axon-capture sidecar, stored in TaskStatus.Results. However, this data is never exposed as Prometheus metrics and there is no mechanism to enforce spending limits. This makes Axon a cost black box in production — operators can see how many tasks ran, but not how much they cost.
This proposal adds (1) cost/token Prometheus metrics to the existing metrics infrastructure, and (2) a BudgetPolicy CRD for namespace or spawner-level spend limits.
Problem
1. Cost data is captured but not observable
The capture sidecar (internal/capture/usage.go) already parses agent output files for all four agent types:
// internal/capture/capture.go:86-91
agentType := os.Getenv("AXON_AGENT_TYPE")
usage := ParseUsage(agentType, usageFile)
for _, key := range []string{"cost-usd", "input-tokens", "output-tokens"} {
if v, ok := usage[key]; ok {
outputs = append(outputs, key+": "+v)
}
}
These values flow into TaskStatus.Results via ResultsFromOutputs() in internal/controller/output_parser.go:44. But the existing Prometheus metrics (internal/controller/metrics.go) only expose:
axon_task_created_total— task count by namespace/typeaxon_task_completed_total— terminal phase countaxon_task_duration_seconds— execution time histogramaxon_reconcile_errors_total— controller errors
No cost or token metrics exist. Operators cannot answer: "How much did we spend on Axon tasks today?" or "Which TaskSpawner is consuming the most tokens?" without manually querying every Task's .status.results map.
2. No spending limits or budget governance
The existing governance primitives are volume-based:
maxConcurrency: limits concurrent tasks (but a single expensive task can cost more than 100 cheap ones)maxTotalTasks: lifetime task count limit (same problem — cost varies wildly per task)activeDeadlineSeconds: time-based timeout (a 5-minute task with opus can cost 10x a 30-minute task with haiku)
None of these control actual spend. An autonomous spawner processing issues with model: opus could burn through hundreds of dollars before anyone notices. This is the #1 concern enterprises raise about autonomous agents.
3. Real-world impact
In the self-development configs:
axon-workers.yamlusesmodel: opuswithmaxConcurrency: 3— three concurrent opus tasks processing GitHub issues. Each could cost $5-50+ depending on complexity, with no spend cap.axon-fake-strategist.yamlruns every 12 hours with opus — roughly $10-30/day with no budget limit.- There's no way to detect or alert on cost anomalies (e.g., a runaway agent in an infinite loop consuming tokens).
Proposed Changes
Part 1: Cost/Token Prometheus Metrics (small, concrete)
Add metrics to internal/controller/metrics.go that emit cost and token data when tasks complete:
var (
// taskCostUSD records the cost in USD of completed Tasks.
taskCostUSD = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "axon_task_cost_usd_total",
Help: "Total cost in USD of completed Tasks",
},
[]string{"namespace", "type", "spawner", "model"},
)
// taskInputTokens records the total input tokens consumed by completed Tasks.
taskInputTokens = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "axon_task_input_tokens_total",
Help: "Total input tokens consumed by completed Tasks",
},
[]string{"namespace", "type", "spawner", "model"},
)
// taskOutputTokens records the total output tokens consumed by completed Tasks.
taskOutputTokens = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "axon_task_output_tokens_total",
Help: "Total output tokens consumed by completed Tasks",
},
[]string{"namespace", "type", "spawner", "model"},
)
)
Instrumentation point — in task_controller.go, when TaskStatus.Results is populated (around line 482):
if results != nil {
spawner := task.Labels["axon.io/taskspawner"]
model := task.Spec.Model
if costStr, ok := results["cost-usd"]; ok {
if cost, err := strconv.ParseFloat(costStr, 64); err == nil {
taskCostUSD.WithLabelValues(task.Namespace, task.Spec.Type, spawner, model).Add(cost)
}
}
if inputStr, ok := results["input-tokens"]; ok {
if tokens, err := strconv.ParseFloat(inputStr, 64); err == nil {
taskInputTokens.WithLabelValues(task.Namespace, task.Spec.Type, spawner, model).Add(tokens)
}
}
if outputStr, ok := results["output-tokens"]; ok {
if tokens, err := strconv.ParseFloat(outputStr, 64); err == nil {
taskOutputTokens.WithLabelValues(task.Namespace, task.Spec.Type, spawner, model).Add(tokens)
}
}
}
Scope: ~40 lines in metrics.go + ~20 lines in task_controller.go. No new dependencies.
Enabled use cases:
# Total spend in the last 24 hours
sum(increase(axon_task_cost_usd_total[24h]))
# Cost per spawner (which spawner is most expensive?)
sum by (spawner) (increase(axon_task_cost_usd_total[24h]))
# Average cost per task by model
sum by (model) (increase(axon_task_cost_usd_total[1h]))
/ sum by (model) (increase(axon_task_completed_total[1h]))
# Alert: spending more than $50/day
sum(increase(axon_task_cost_usd_total[24h])) > 50
# Token consumption rate (useful for API rate planning)
rate(axon_task_input_tokens_total[1h])
Part 2: BudgetPolicy CRD (medium, high-impact)
A new CRD that enables cost-based governance:
// BudgetPolicy defines spending limits for Tasks within a scope.
type BudgetPolicy struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BudgetPolicySpec `json:"spec,omitempty"`
Status BudgetPolicyStatus `json:"status,omitempty"`
}
type BudgetPolicySpec struct {
// Scope defines what this budget applies to.
// +kubebuilder:validation:Required
Scope BudgetScope `json:"scope"`
// Limits defines the spending limits.
// +kubebuilder:validation:Required
Limits BudgetLimits `json:"limits"`
// Window is the rolling time window for budget calculation.
// Supported formats: "1h", "24h", "7d", "30d".
// +kubebuilder:validation:Required
// +kubebuilder:validation:Pattern="^[0-9]+(h|d)$"
Window string `json:"window"`
// Action defines what happens when the budget is exceeded.
// +kubebuilder:validation:Enum=Suspend;Warn
// +kubebuilder:default=Warn
// +optional
Action string `json:"action,omitempty"`
}
type BudgetScope struct {
// Namespace scopes the budget to a specific namespace.
// If empty, applies to the namespace where BudgetPolicy is created.
// +optional
Namespace string `json:"namespace,omitempty"`
// TaskSpawnerRef scopes the budget to a specific TaskSpawner.
// +optional
TaskSpawnerRef *string `json:"taskSpawnerRef,omitempty"`
}
type BudgetLimits struct {
// MaxCostUSD is the maximum total cost in USD within the window.
// +optional
// +kubebuilder:validation:Pattern="^[0-9]+(\\.[0-9]+)?$"
MaxCostUSD *string `json:"maxCostUSD,omitempty"`
// MaxInputTokens is the maximum total input tokens within the window.
// +optional
MaxInputTokens *int64 `json:"maxInputTokens,omitempty"`
// MaxOutputTokens is the maximum total output tokens within the window.
// +optional
MaxOutputTokens *int64 `json:"maxOutputTokens,omitempty"`
}
type BudgetPolicyStatus struct {
// CurrentSpend tracks actual spending in the current window.
// +optional
CurrentCostUSD string `json:"currentCostUSD,omitempty"`
// CurrentInputTokens tracks actual input tokens in the current window.
// +optional
CurrentInputTokens int64 `json:"currentInputTokens,omitempty"`
// CurrentOutputTokens tracks actual output tokens in the current window.
// +optional
CurrentOutputTokens int64 `json:"currentOutputTokens,omitempty"`
// Exceeded is true when any limit has been reached.
// +optional
Exceeded bool `json:"exceeded,omitempty"`
// LastUpdated is when the budget status was last calculated.
// +optional
LastUpdated *metav1.Time `json:"lastUpdated,omitempty"`
}
Example usage:
# Limit total daily spend across all tasks in the namespace
apiVersion: axon.io/v1alpha1
kind: BudgetPolicy
metadata:
name: daily-budget
spec:
scope: {} # namespace-scoped (uses the namespace where this resource lives)
limits:
maxCostUSD: "100"
window: "24h"
action: Suspend # auto-suspend TaskSpawners when budget exceeded
---
# Limit a specific spawner's weekly spend
apiVersion: axon.io/v1alpha1
kind: BudgetPolicy
metadata:
name: workers-weekly-budget
spec:
scope:
taskSpawnerRef: axon-workers
limits:
maxCostUSD: "500"
maxOutputTokens: 10000000 # 10M output tokens
window: "7d"
action: Warn # emit a Kubernetes Event warning but don't stop
Implementation approach for BudgetPolicy
The BudgetPolicyReconciler would:
- Periodically query completed Tasks within the scope and window
- Sum
cost-usd,input-tokens,output-tokensfromTaskStatus.Results - Update
BudgetPolicyStatuswith current totals - When
action: Suspendand limits exceeded:- Set
spec.suspend: trueon matching TaskSpawners - Record a Kubernetes Event on both the BudgetPolicy and the TaskSpawner
- Set
- When the window rolls over and spend drops below limits:
- Clear the
Exceededflag - Optionally auto-resume suspended TaskSpawners (configurable)
- Clear the
This leverages the existing suspend field on TaskSpawner (added in #326) for enforcement.
Implementation Priority
- Phase 1: Cost Prometheus metrics — ~60 lines of code, immediate value, no new CRDs. Enables Grafana dashboards and Prometheus alerts for cost monitoring.
- Phase 2: BudgetPolicy CRD — New CRD + controller. Enables automated cost governance. Depends on Phase 1 data being available.
Phase 1 alone provides substantial value and can be shipped independently.
Why this matters for adoption
Every enterprise evaluation of autonomous agents asks the same question: "How do we prevent runaway costs?" The current answer with Axon is "set maxConcurrency and activeDeadlineSeconds" — but these are indirect, imprecise proxies for cost. A single 3-minute opus task can cost more than ten 30-minute sonnet tasks.
Cost metrics + budget governance makes Axon production-ready for organizations where multiple teams run autonomous agents and need chargeback, accountability, and safety guardrails.
Backward Compatibility
- Phase 1: Purely additive metrics — no API changes, no behavior changes
- Phase 2: New CRD — no changes to existing resources. BudgetPolicy is opt-in; clusters without BudgetPolicy resources behave identically to today
References
- Capture sidecar cost extraction:
internal/capture/capture.go:86-91 - Usage parsing (all 4 agents):
internal/capture/usage.go - Existing Prometheus metrics:
internal/controller/metrics.go - Results stored in status:
internal/controller/task_controller.go:482 - Results parsed from outputs:
internal/controller/output_parser.go:44(ResultsFromOutputs) - TaskSpawner suspend field:
api/v1alpha1/taskspawner_types.go(implemented via #326) - Self-dev workers (opus, concurrent):
self-development/axon-workers.yaml:17 - Self-dev strategist (opus, cron):
self-development/axon-fake-strategist.yaml:14
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with internal/controller/metrics.go and internal/controller/task_controller.go around line 482, then review internal/controller/output_parser.go and the TaskSpawner API. Separate the concrete metrics phase from the larger BudgetPolicy CRD and controller design. Done means cost and token usage are exposed from task results, with budget limits and enforcement behavior specified and covered for the proposed scopes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, kubernetes, prometheus
- Domain
- backend-api-design, devops, observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100