Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion internal/controller/usagereport_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
internalapi "github.com/defilantech/infercost/internal/api"
"github.com/defilantech/infercost/internal/calculator"
"github.com/defilantech/infercost/internal/metrics"
reporting "github.com/defilantech/infercost/internal/report"
"github.com/defilantech/infercost/internal/scraper"
"github.com/defilantech/infercost/internal/utilization"
)
Expand Down Expand Up @@ -526,7 +527,7 @@ func (r *UsageReportReconciler) applyStatusIfChanged(ctx context.Context, report
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "ReportComputed",
Message: fmt.Sprintf("Period %s: $%.4f across %d tokens", c.period, c.totalCost, totalTokens),
Message: reporting.StatusMessage(report.Status),
LastTransitionTime: metaNow,
})

Expand Down
4 changes: 4 additions & 0 deletions internal/controller/usagereport_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ var _ = Describe("UsageReport Controller", func() {
Expect(readyCondition).NotTo(BeNil())
Expect(readyCondition.Status).To(Equal(metav1.ConditionTrue))
Expect(readyCondition.Reason).To(Equal("ReportComputed"))
By("verifying the utilization-aware framing (issue #41)")
Expect(readyCondition.Message).To(ContainSubstring("Amortized:"))
Expect(readyCondition.Message).To(ContainSubstring("utilization"))
Expect(readyCondition.Message).To(ContainSubstring("Break-even with"))
})
})

Expand Down
72 changes: 72 additions & 0 deletions internal/report/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Package report renders human-facing summaries of UsageReport status, shared
// by the controller (status condition message) and the CLI so their framing
// stays identical.
package report

import (
"fmt"
"strings"

finopsv1alpha1 "github.com/defilantech/infercost/api/v1alpha1"
)

// StatusMessage renders the utilization-aware framing for a UsageReport: the
// raw period cost, the amortized $/MTok with its utilization context, the
// marginal $/MTok (when known), and the break-even comparison against the first
// configured cloud target. Surfacing utilization inline keeps the amortized
// number from reading as "worse than every cloud" at low utilization.
func StatusMessage(s finopsv1alpha1.UsageReportStatus) string {
totalTokens := s.InputTokens + s.OutputTokens
var b strings.Builder

fmt.Fprintf(&b, "Period %s: $%.4f across %s tokens.", s.Period, s.EstimatedCostUSD, withCommas(totalTokens))
fmt.Fprintf(&b, "\nAmortized: $%.2f/MTok at %.0f%% utilization.", s.CostPerMillionTokens, s.UtilizationPercent)

// Marginal is omitted when zero: that means no active energy was sampled, so
// "$0.0000/MTok" would be misleading rather than honest.
if s.MarginalCostPerMillionTokens > 0 {
fmt.Fprintf(&b, "\nMarginal: $%.4f/MTok.", s.MarginalCostPerMillionTokens)
}

if len(s.BreakEvenAnalysis) > 0 {
be := s.BreakEvenAnalysis[0]
fmt.Fprintf(&b, "\nBreak-even with %s/%s: %s tokens/day (you served %s).",
be.Provider, be.Model,
humanizeTokens(be.BreakEvenTokensPerDay),
humanizeTokens(be.CurrentUtilizationTokensPerDay))
}

return b.String()
}

// humanizeTokens renders a token count compactly: 950, 6K, 142K, 1.2M.
func humanizeTokens(n int64) string {
switch {
case n >= 1_000_000:
return strings.TrimSuffix(fmt.Sprintf("%.1f", float64(n)/1_000_000), ".0") + "M"
case n >= 1_000:
return strings.TrimSuffix(fmt.Sprintf("%.1f", float64(n)/1_000), ".0") + "K"
default:
return fmt.Sprintf("%d", n)
}
}

// withCommas formats an integer with thousands separators (3993 -> "3,993").
func withCommas(n int64) string {
s := fmt.Sprintf("%d", n)
neg := strings.HasPrefix(s, "-")
if neg {
s = s[1:]
}
var out []byte
for i := 0; i < len(s); i++ {
if i > 0 && (len(s)-i)%3 == 0 {
out = append(out, ',')
}
out = append(out, s[i])
}
if neg {
return "-" + string(out)
}
return string(out)
}
81 changes: 81 additions & 0 deletions internal/report/message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package report

import (
"strings"
"testing"

finopsv1alpha1 "github.com/defilantech/infercost/api/v1alpha1"
)

func fullStatus() finopsv1alpha1.UsageReportStatus {
return finopsv1alpha1.UsageReportStatus{
Period: "2026-04-23",
EstimatedCostUSD: 0.0660,
InputTokens: 2000,
OutputTokens: 1993,
CostPerMillionTokens: 16.52,
MarginalCostPerMillionTokens: 0.004,
UtilizationPercent: 4.0,
BreakEvenAnalysis: []finopsv1alpha1.BreakEvenEntry{
{Provider: "Anthropic", Model: "claude-opus-4-6",
BreakEvenTokensPerDay: 142000, CurrentUtilizationTokensPerDay: 6000,
PercentOfBreakEven: 4.2, Verdict: "cloud-cheaper-at-current-utilization"},
},
}
}

func TestStatusMessage_FullFraming(t *testing.T) {
msg := StatusMessage(fullStatus())
wants := []string{
"Period 2026-04-23: $0.0660 across 3,993 tokens.",
"Amortized: $16.52/MTok at 4% utilization.",
"Marginal: $0.0040/MTok.",
"Break-even with Anthropic/claude-opus-4-6: 142K tokens/day (you served 6K).",
}
for _, w := range wants {
if !strings.Contains(msg, w) {
t.Errorf("message missing line %q\ngot:\n%s", w, msg)
}
}
}

func TestStatusMessage_NoBreakEvenLineWhenEmpty(t *testing.T) {
s := fullStatus()
s.BreakEvenAnalysis = nil
msg := StatusMessage(s)
if strings.Contains(msg, "Break-even") {
t.Errorf("expected no break-even line when analysis is empty; got:\n%s", msg)
}
}

func TestStatusMessage_NoMarginalLineWhenZero(t *testing.T) {
s := fullStatus()
s.MarginalCostPerMillionTokens = 0
msg := StatusMessage(s)
if strings.Contains(msg, "Marginal:") {
t.Errorf("expected no marginal line when marginal is 0; got:\n%s", msg)
}
}

func TestHumanizeTokens(t *testing.T) {
cases := map[int64]string{
950: "950",
6000: "6K",
142000: "142K",
1200000: "1.2M",
}
for in, want := range cases {
if got := humanizeTokens(in); got != want {
t.Errorf("humanizeTokens(%d) = %q, want %q", in, got, want)
}
}
}

func TestWithCommas(t *testing.T) {
if got := withCommas(3993); got != "3,993" {
t.Errorf("withCommas(3993) = %q, want 3,993", got)
}
if got := withCommas(1000000); got != "1,000,000" {
t.Errorf("withCommas(1000000) = %q, want 1,000,000", got)
}
}
24 changes: 24 additions & 0 deletions pkg/cli/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"
"text/tabwriter"
"time"

Expand All @@ -14,6 +15,7 @@ import (

finopsv1alpha1 "github.com/defilantech/infercost/api/v1alpha1"
"github.com/defilantech/infercost/internal/calculator"
reporting "github.com/defilantech/infercost/internal/report"
"github.com/defilantech/infercost/internal/scraper"
)

Expand Down Expand Up @@ -219,6 +221,28 @@ func runStatus(opts *statusOptions) error {
_ = w.Flush()
}

// Utilization-aware framing per UsageReport (issue #41): surfaces the
// amortized/marginal/break-even context inline so the headline cost isn't
// misread as "worse than cloud" at low utilization.
var reports finopsv1alpha1.UsageReportList
if err := k8sClient.List(ctx, &reports); err == nil && len(reports.Items) > 0 {
printed := false
for _, rep := range reports.Items {
if rep.Status.Period == "" {
continue // not computed yet
}
if !printed {
fmt.Println("\nUSAGE REPORTS")
fmt.Println("=============")
printed = true
}
fmt.Printf("\n%s:\n", rep.Name)
for line := range strings.SplitSeq(reporting.StatusMessage(rep.Status), "\n") {
fmt.Printf(" %s\n", line)
}
}
}

return nil
}

Expand Down