@@ -13,14 +13,39 @@ import Foundation
1313final class ActivityStore : ObservableObject {
1414 struct Stat : Codable , Equatable {
1515 var requests : Int = 0
16+ /// Total tokens = input + output. Kept for the heatmap/back-compat.
1617 var tokens : Int = 0
18+ var inputTokens : Int = 0
19+ var outputTokens : Int = 0
20+
21+ init ( requests: Int = 0 , tokens: Int = 0 , inputTokens: Int = 0 , outputTokens: Int = 0 ) {
22+ self . requests = requests
23+ self . tokens = tokens
24+ self . inputTokens = inputTokens
25+ self . outputTokens = outputTokens
26+ }
27+
28+ /// Tolerates older files that only had `requests`/`tokens` (split fields absent).
29+ init ( from decoder: Decoder ) throws {
30+ let c = try decoder. container ( keyedBy: CodingKeys . self)
31+ requests = ( try ? c. decode ( Int . self, forKey: . requests) ) ?? 0
32+ tokens = ( try ? c. decode ( Int . self, forKey: . tokens) ) ?? 0
33+ inputTokens = ( try ? c. decode ( Int . self, forKey: . inputTokens) ) ?? 0
34+ outputTokens = ( try ? c. decode ( Int . self, forKey: . outputTokens) ) ?? 0
35+ }
1736 }
1837
1938 /// The metric the dashboard is currently showing.
2039 enum Unit : String , CaseIterable , Identifiable {
21- case requests, tokens
40+ case requests, tokens, cost
2241 var id : String { rawValue }
23- var title : String { self == . requests ? " Requests " : " Tokens " }
42+ var title : String {
43+ switch self {
44+ case . requests: return " Requests "
45+ case . tokens: return " Tokens "
46+ case . cost: return " Cost "
47+ }
48+ }
2449 }
2550
2651 /// day (yyyy-MM-dd, local) -> model id -> stat
@@ -50,16 +75,20 @@ final class ActivityStore: ObservableObject {
5075 unit == . requests ? stat. requests : stat. tokens
5176 }
5277
53- /// Records a batch of per-model request counts and token counts on the current local
54- /// day with a single save.
55- func record( requests: [ String : Int ] , tokens : [ String : Int ] , on date: Date = Date ( ) ) {
56- let models = Set ( requests. keys) . union ( tokens . keys)
78+ /// Records a batch of per-model request counts and input/output token counts on the
79+ /// current local day with a single save.
80+ func record( requests: [ String : Int ] , inputTokens : [ String : Int ] , outputTokens : [ String : Int ] , on date: Date = Date ( ) ) {
81+ let models = Set ( requests. keys) . union ( inputTokens . keys ) . union ( outputTokens . keys)
5782 guard !models. isEmpty else { return }
5883 let key = Self . dayKey ( date)
5984 for model in models {
6085 var stat = days [ key] ? [ model] ?? Stat ( )
86+ let inTok = max ( 0 , inputTokens [ model] ?? 0 )
87+ let outTok = max ( 0 , outputTokens [ model] ?? 0 )
6188 stat. requests += max ( 0 , requests [ model] ?? 0 )
62- stat. tokens += max ( 0 , tokens [ model] ?? 0 )
89+ stat. inputTokens += inTok
90+ stat. outputTokens += outTok
91+ stat. tokens += inTok + outTok
6392 days [ key, default: [ : ] ] [ model] = stat
6493 }
6594 save ( )
@@ -94,6 +123,36 @@ final class ActivityStore: ObservableObject {
94123 /// Number of distinct days with at least one recorded model.
95124 var activeDays : Int { days. values. filter { !$0. isEmpty } . count }
96125
126+ // MARK: Cost aggregates (USD, derived from token split × ModelPricing)
127+
128+ private func cost( _ model: String , _ stat: Stat ) -> Double {
129+ ModelPricing . cost ( model: model, inputTokens: stat. inputTokens, outputTokens: stat. outputTokens)
130+ }
131+
132+ /// Estimated equivalent API cost (USD) across all history.
133+ func costTotal( ) -> Double {
134+ days. values. reduce ( 0 ) { acc, byModel in
135+ acc + byModel. reduce ( 0 ) { $0 + cost( $1. key, $1. value) }
136+ }
137+ }
138+
139+ /// Estimated cost for a given local day.
140+ func cost( on date: Date ) -> Double {
141+ ( days [ Self . dayKey ( date) ] ? . reduce ( 0 ) { $0 + cost( $1. key, $1. value) } ) ?? 0
142+ }
143+
144+ /// Today's estimated cost.
145+ func costToday( ) -> Double { cost ( on: Date ( ) ) }
146+
147+ /// Per-model cost totals (USD), highest first.
148+ func modelCostTotals( ) -> [ ( model: String , cost: Double ) ] {
149+ var totals : [ String : Double ] = [ : ]
150+ for byModel in days. values {
151+ for (model, stat) in byModel { totals [ model, default: 0 ] += cost ( model, stat) }
152+ }
153+ return totals. sorted { $0. value > $1. value } . map { ( model: $0. key, cost: $0. value) }
154+ }
155+
97156 // MARK: Persistence
98157
99158 private func load( ) {
0 commit comments