Skip to content

Commit 3074852

Browse files
committed
fix(parser): adapt omnigent costs to money.Money
Main migrated ParsedUsageEvent.CostUSD to Cost *money.Money (authoritative microdollars, #1224) under this branch. Keep the wire-format float math for per-model apportionment and convert at the event boundary via omnigentCost, which drops unconvertible values (negative, non-finite) so catalog pricing applies, matching the parser's fail-soft posture.
1 parent fcb864a commit 3074852

3 files changed

Lines changed: 44 additions & 22 deletions

File tree

internal/parser/omnigent.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020

2121
"github.com/klauspost/compress/zstd"
2222
_ "github.com/mattn/go-sqlite3"
23+
24+
"go.kenn.io/agentsview/internal/money"
2325
)
2426

2527
// omnigent orchestrates other coding agents (Claude Code, Codex, OpenCode, ...)
@@ -1087,6 +1089,21 @@ func omnigentSystemLine(typeName, data, searchText string) string {
10871089
return ""
10881090
}
10891091

1092+
// omnigentCost converts a wire-format dollar float to Money at the event
1093+
// boundary. A nil input stays nil so catalog-based token pricing applies;
1094+
// an unconvertible value (negative, non-finite) is dropped the same way,
1095+
// matching the parser's fail-soft posture toward malformed usage blobs.
1096+
func omnigentCost(value *float64) *money.Money {
1097+
if value == nil {
1098+
return nil
1099+
}
1100+
cost, err := money.FromFloatDollars(*value)
1101+
if err != nil {
1102+
return nil
1103+
}
1104+
return &cost
1105+
}
1106+
10901107
// omnigentUsageEvents decodes the session_usage blob (zstd-framed on newer
10911108
// builds, plaintext JSON on older ones) into a single session-level usage
10921109
// event, plus per-model breakdown when present.
@@ -1138,7 +1155,7 @@ func omnigentUsageEvents(
11381155
Model: model,
11391156
InputTokens: m.InputTokens,
11401157
OutputTokens: m.OutputTokens,
1141-
CostUSD: m.TotalCostUSD,
1158+
Cost: omnigentCost(m.TotalCostUSD),
11421159
DedupKey: sessionID + "|usage|" + model,
11431160
})
11441161
if m.TotalCostUSD != nil {
@@ -1167,7 +1184,7 @@ func omnigentUsageEvents(
11671184
float64(len(missingCostIndexes)-i)
11681185
}
11691186
}
1170-
events[eventIndex].CostUSD = &cost
1187+
events[eventIndex].Cost = omnigentCost(&cost)
11711188
remainingCost -= cost
11721189
remainingWeight -= max(
11731190
0,
@@ -1189,7 +1206,7 @@ func omnigentUsageEvents(
11891206
Model: fallbackModel,
11901207
InputTokens: usage.InputTokens,
11911208
OutputTokens: usage.OutputTokens,
1192-
CostUSD: usage.TotalCostUSD,
1209+
Cost: omnigentCost(usage.TotalCostUSD),
11931210
DedupKey: sessionID + "|usage|" + fallbackModel,
11941211
}}
11951212
}

internal/parser/omnigent_test.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"testing"
1919
"time"
2020

21+
"go.kenn.io/agentsview/internal/money"
22+
2123
"github.com/klauspost/compress/zstd"
2224
_ "github.com/mattn/go-sqlite3"
2325
"github.com/stretchr/testify/assert"
@@ -388,8 +390,9 @@ func assertOmnigentParse(t *testing.T, results []ParseResult) {
388390
assert.Equal(t, "claude-opus-4-8", root.UsageEvents[0].Model)
389391
assert.Equal(t, 100, root.UsageEvents[0].InputTokens)
390392
assert.Equal(t, 50, root.UsageEvents[0].OutputTokens)
391-
require.NotNil(t, root.UsageEvents[0].CostUSD)
392-
assert.InDelta(t, 1.5, *root.UsageEvents[0].CostUSD, 0.0001)
393+
require.NotNil(t, root.UsageEvents[0].Cost)
394+
assert.Equal(t, money.Money{Microdollars: 1_500_000},
395+
*root.UsageEvents[0].Cost)
393396
assert.True(t, root.Session.HasTotalOutputTokens)
394397
assert.Equal(t, 50, root.Session.TotalOutputTokens)
395398
assert.False(t, root.Session.HasPeakContextTokens)
@@ -1480,7 +1483,7 @@ func TestOmnigentBinaryIDGenerationParses(t *testing.T) {
14801483
require.Len(t, main.UsageEvents, 1,
14811484
"framed session_usage must decode into usage events")
14821485
assert.Equal(t, "omnigent-large", main.UsageEvents[0].Model)
1483-
assert.Nil(t, main.UsageEvents[0].CostUSD,
1486+
assert.Nil(t, main.UsageEvents[0].Cost,
14841487
"absent total_cost_usd must stay nil so catalog pricing applies")
14851488

14861489
sub, ok := byID[omnigentIDPrefix+"0:"+omnigentBinarySubHex]
@@ -1580,7 +1583,7 @@ func TestOmnigentUsageEventsTrackCostPresence(t *testing.T) {
15801583
name string
15811584
payload string
15821585
model string
1583-
wantCost *float64
1586+
wantCost *money.Money
15841587
}{
15851588
{
15861589
name: "aggregate without cost stays nil",
@@ -1598,14 +1601,14 @@ func TestOmnigentUsageEventsTrackCostPresence(t *testing.T) {
15981601
payload: `{"by_model":{"m1":` +
15991602
`{"input_tokens":10,"output_tokens":5,"total_cost_usd":0}}}`,
16001603
model: "m1",
1601-
wantCost: new(float64),
1604+
wantCost: &money.Money{},
16021605
},
16031606
{
16041607
name: "recorded cost is preserved",
16051608
payload: `{"input_tokens":10,"output_tokens":5,` +
16061609
`"total_cost_usd":1.25}`,
16071610
model: "fallback",
1608-
wantCost: func() *float64 { v := 1.25; return &v }(),
1611+
wantCost: &money.Money{Microdollars: 1_250_000},
16091612
},
16101613
} {
16111614
t.Run(tc.name, func(t *testing.T) {
@@ -1616,12 +1619,12 @@ func TestOmnigentUsageEventsTrackCostPresence(t *testing.T) {
16161619
assert.Equal(t, tc.model, events[0].Model)
16171620
assert.Equal(t, 10, events[0].InputTokens)
16181621
if tc.wantCost == nil {
1619-
assert.Nil(t, events[0].CostUSD,
1622+
assert.Nil(t, events[0].Cost,
16201623
"unknown cost must stay NULL for catalog pricing")
16211624
return
16221625
}
1623-
require.NotNil(t, events[0].CostUSD)
1624-
assert.InDelta(t, *tc.wantCost, *events[0].CostUSD, 0.0001)
1626+
require.NotNil(t, events[0].Cost)
1627+
assert.Equal(t, *tc.wantCost, *events[0].Cost)
16251628
})
16261629
}
16271630
}
@@ -1641,12 +1644,13 @@ func TestOmnigentUsageEventsAllocateAggregateCostAcrossModels(t *testing.T) {
16411644

16421645
require.Len(t, events, 2)
16431646
assert.Equal(t, "large", events[0].Model)
1644-
require.NotNil(t, events[0].CostUSD)
1645-
assert.InDelta(t, 3, *events[0].CostUSD, 0.0001)
1647+
require.NotNil(t, events[0].Cost)
1648+
assert.Equal(t, money.Money{Microdollars: 3_000_000}, *events[0].Cost)
16461649
assert.Equal(t, "small", events[1].Model)
1647-
require.NotNil(t, events[1].CostUSD)
1648-
assert.InDelta(t, 1, *events[1].CostUSD, 0.0001)
1649-
assert.InDelta(t, 4, *events[0].CostUSD+*events[1].CostUSD, 0.0001,
1650+
require.NotNil(t, events[1].Cost)
1651+
assert.Equal(t, money.Money{Microdollars: 1_000_000}, *events[1].Cost)
1652+
assert.Equal(t, int64(4_000_000),
1653+
events[0].Cost.Microdollars+events[1].Cost.Microdollars,
16501654
"per-model events must retain Omnigent's authoritative aggregate cost")
16511655
}
16521656

@@ -1669,11 +1673,11 @@ func TestOmnigentUsageEventsAllocateAggregateRemainder(t *testing.T) {
16691673

16701674
require.Len(t, events, 2)
16711675
assert.Equal(t, "priced", events[0].Model)
1672-
require.NotNil(t, events[0].CostUSD)
1673-
assert.InDelta(t, 1, *events[0].CostUSD, 0.0001)
1676+
require.NotNil(t, events[0].Cost)
1677+
assert.Equal(t, money.Money{Microdollars: 1_000_000}, *events[0].Cost)
16741678
assert.Equal(t, "unpriced", events[1].Model)
1675-
require.NotNil(t, events[1].CostUSD)
1676-
assert.InDelta(t, 2, *events[1].CostUSD, 0.0001)
1679+
require.NotNil(t, events[1].Cost)
1680+
assert.Equal(t, money.Money{Microdollars: 2_000_000}, *events[1].Cost)
16771681
}
16781682

16791683
func TestOmnigentShmEventDoesNotResolveToContainer(t *testing.T) {

internal/sync/omnigent_integration_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"go.kenn.io/agentsview/internal/db"
2020
"go.kenn.io/agentsview/internal/dbtest"
21+
"go.kenn.io/agentsview/internal/money"
2122
"go.kenn.io/agentsview/internal/parser"
2223
"go.kenn.io/agentsview/internal/sync"
2324
"go.kenn.io/agentsview/internal/testjsonl"
@@ -1150,7 +1151,7 @@ func TestSyncOmnigentFallbackUsageAppearsInAnalytics(t *testing.T) {
11501151
require.Len(t, daily.Daily, 1)
11511152
assert.Equal(t, 120, daily.Daily[0].InputTokens)
11521153
assert.Equal(t, 30, daily.Daily[0].OutputTokens)
1153-
assert.InDelta(t, 0.25, daily.Daily[0].TotalCost, 0.0001)
1154+
assert.Equal(t, money.Money{Microdollars: 250_000}, daily.Daily[0].TotalCost)
11541155
}
11551156

11561157
func TestSyncOmnigentInPlaceEditIsReconciledByFullSync(t *testing.T) {

0 commit comments

Comments
 (0)