@@ -10,23 +10,240 @@ import SwiftData
1010
1111@main
1212struct OpenAIAPIUsageApp : App {
13- var sharedModelContainer : ModelContainer = {
14- let schema = Schema ( [
15- Item . self,
16- ] )
17- let modelConfiguration = ModelConfiguration ( schema: schema, isStoredInMemoryOnly: false )
13+ @NSApplicationDelegateAdaptor ( AppDelegate . self) var appDelegate
14+
15+ var body : some Scene {
16+ Settings {
17+ Text ( " Settings " )
18+ }
19+ }
20+ }
21+
22+ class AppDelegate : NSObject , NSApplicationDelegate {
23+ let BEARER_TOKEN_KEY = " BearerToken "
24+ var statusItem : NSStatusItem ?
25+ private var websiteItem : NSMenuItem ?
26+ var textField : NSTextField !
27+
28+ func applicationDidFinishLaunching( _ notification: Notification ) {
29+ statusItem = NSStatusBar . system. statusItem ( withLength: NSStatusItem . variableLength)
30+
31+ let statusBarMenu = NSMenu ( title: " Menu " )
32+
33+ websiteItem = statusBarMenu. addItem (
34+ withTitle: " Show Details " ,
35+ action: #selector( gotoWebsite ( sender: ) ) ,
36+ keyEquivalent: " "
37+ )
38+
39+ statusBarMenu. addItem ( . separator( ) )
1840
19- do {
20- return try ModelContainer ( for: schema, configurations: [ modelConfiguration] )
21- } catch {
22- fatalError ( " Could not create ModelContainer: \( error) " )
41+ let customView = NSView ( frame: NSRect ( x: 0 , y: 0 , width: 340 , height: 20 ) )
42+
43+ let label = NSTextField ( frame: NSRect ( x: 12 , y: 0 , width: 100 , height: 20 ) )
44+ label. stringValue = " Bearer Token "
45+ label. isBezeled = false
46+ label. drawsBackground = false
47+ label. isEditable = false
48+ label. sizeToFit ( )
49+
50+ textField = NSTextField ( frame: NSRect ( x: 105 , y: 0 , width: 230 , height: 20 ) )
51+ textField. isEditable = false
52+ textField. stringValue = getBearerToken ( )
53+
54+ customView. addSubview ( label)
55+ customView. addSubview ( textField)
56+
57+ let tokenMenuItem = NSMenuItem ( )
58+ tokenMenuItem. view = customView
59+
60+ statusBarMenu. addItem ( tokenMenuItem)
61+
62+ statusBarMenu. addItem (
63+ withTitle: " Get Bearer Token " ,
64+ action: #selector( gotoWebsite ( sender: ) ) ,
65+ keyEquivalent: " "
66+ )
67+
68+ statusBarMenu. addItem (
69+ withTitle: " Paste Bearer Token " ,
70+ action: #selector( pasteBearerToken) ,
71+ keyEquivalent: " "
72+ )
73+
74+ statusBarMenu. addItem ( . separator( ) )
75+
76+ statusBarMenu. addItem (
77+ withTitle: " Quit " ,
78+ action: #selector( quit ( sender: ) ) ,
79+ keyEquivalent: " "
80+ )
81+ statusItem? . menu = statusBarMenu
82+
83+ statusItem? . button? . title = getBearerToken ( ) . isEmpty ? " No Bearer Token " : " Updating... "
84+ update ( )
85+ WatchDog . shared. startRepeatingTimer ( 60 ) {
86+ self . update ( )
2387 }
24- } ( )
88+ }
89+
90+ @objc func gotoWebsite( sender: Any ) {
91+ let urlString = " https://platform.openai.com/usage "
92+ if let url = URL ( string: urlString) {
93+ NSWorkspace . shared. open ( url)
94+ }
95+ }
96+
97+ func getBearerToken( ) -> String {
98+ return UserDefaults . standard. string ( forKey: BEARER_TOKEN_KEY) ?? " "
99+ }
100+
101+ func setBearerToken( _ value: String ) {
102+ UserDefaults . standard. set ( value, forKey: BEARER_TOKEN_KEY)
103+ }
104+
105+ @objc func pasteBearerToken( _ sender: AnyObject ) {
106+ if let token = fetchBearTokenFromNSPasteboard ( ) {
107+ let value = token. trimmingCharacters ( in: . whitespacesAndNewlines)
108+ textField. stringValue = value
109+ setBearerToken ( value)
110+ update ( )
111+ }
112+ }
113+
114+ private func fetchBearTokenFromNSPasteboard( ) -> String ? {
115+ let pasteboard = NSPasteboard . general
116+ if let text = pasteboard. string ( forType: . string) , text. hasPrefix ( " sess- " ) {
117+ return text
118+ }
119+ return nil
120+ }
121+
122+ @objc func quit( sender: AnyObject ) {
123+ NSApplication . shared. terminate ( self )
124+ }
125+
126+ func update( ) {
127+ let bearerToken = getBearerToken ( )
128+ guard !bearerToken. isEmpty else {
129+ return
130+ }
131+
132+ Task {
133+ let amount = await getUsage ( bearerToken)
134+ DispatchQueue . main. async {
135+ self . updateBillAmount ( amount)
136+ }
137+ }
138+ }
139+
140+ func updateBillAmount( _ amount: Double ? ) {
141+ let title = amount != nil ? String ( format: " $%.2f " , amount!/ 100.0 ) : " USD? "
142+ statusItem? . button? . title = title
143+ }
144+ }
25145
26- var body : some Scene {
27- WindowGroup {
28- ContentView ( )
146+ class WatchDog {
147+ static var shared : WatchDog = WatchDog ( )
148+
149+ var timer : Timer ?
150+
151+ func startRepeatingTimer( _ interval: TimeInterval , action: @escaping ( ) -> Void ) {
152+ timer = Timer . scheduledTimer ( withTimeInterval: interval, repeats: true ) { _ in
153+ action ( )
29154 }
30- . modelContainer ( sharedModelContainer)
155+ }
156+
157+ func stopTimer( ) {
158+ timer? . invalidate ( )
159+ timer = nil
160+ }
161+ }
162+
163+ func getUsage( _ bearerToken: String ) async -> Double ? {
164+ let ( startDate, endDate) = getFirstDaysOfCurrentAndNextMonth ( ) //getTodaysAndNextMonthsFirstDate()
165+ print ( startDate, endDate)
166+ let urlString = " https://api.openai.com/dashboard/billing/usage?end_date= \( endDate) &start_date= \( startDate) "
167+ guard let url = URL ( string: urlString) else { return nil }
168+ var request = URLRequest ( url: url)
169+ request. httpMethod = " GET "
170+ request. addValue ( " Bearer \( bearerToken) " , forHTTPHeaderField: " Authorization " )
171+ request. addValue ( " application/json " , forHTTPHeaderField: " Content-Type " )
172+ request. addValue ( " Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3 " , forHTTPHeaderField: " User-Agent " )
173+
174+ do {
175+ let ( data, _) = try await URLSession . shared. data ( for: request)
176+ let decoder = JSONDecoder ( )
177+ let usage = try decoder. decode ( Usage . self, from: data)
178+ return usage. totalUsage
179+ } catch {
180+ print ( error. localizedDescription)
181+ return nil
182+ }
183+ }
184+
185+ func getFirstDaysOfCurrentAndNextMonth( ) -> ( String , String ) {
186+ let dateFormatter = DateFormatter ( )
187+ dateFormatter. dateFormat = " yyyy-MM-dd "
188+
189+ let now = Date ( )
190+ let calendar = Calendar . current
191+
192+ let currentMonthComponents = calendar. dateComponents ( [ . year, . month] , from: now)
193+ let startOfCurrentMonth = calendar. date ( from: currentMonthComponents) !
194+
195+ var nextMonthComponents = DateComponents ( )
196+ nextMonthComponents. month = 1
197+ let startOfNextMonth = calendar. date ( byAdding: nextMonthComponents, to: startOfCurrentMonth) !
198+
199+ return ( dateFormatter. string ( from: startOfCurrentMonth) , dateFormatter. string ( from: startOfNextMonth) )
200+ }
201+
202+ func getTodaysAndNextMonthsFirstDate( ) -> ( today: String , firstDayOfNextMonth: String ) {
203+ let dateFormatter = DateFormatter ( )
204+ dateFormatter. dateFormat = " yyyy-MM-dd "
205+
206+ let today = Date ( )
207+ var calendar = Calendar . current
208+ calendar. timeZone = TimeZone ( identifier: " UTC " ) ?? . current
209+
210+ var components = DateComponents ( )
211+ components. month = 1
212+ components. day = - ( ( calendar. component ( . day, from: today) - 1 ) )
213+
214+ let firstDayNextMonth = calendar. date ( byAdding: components, to: today) !
215+
216+ // Reset to the first day of the next month
217+ let componentsForNextMonth = calendar. dateComponents ( [ . year, . month] , from: firstDayNextMonth)
218+ let firstDayOfNextMonth = calendar. date ( from: componentsForNextMonth) !
219+
220+ return ( dateFormatter. string ( from: today) , dateFormatter. string ( from: firstDayOfNextMonth) )
221+ }
222+
223+ struct Usage : Codable {
224+ let object : String
225+ let dailyCosts : [ DailyCost ]
226+ let totalUsage : Double
227+
228+ enum CodingKeys : String , CodingKey {
229+ case object
230+ case dailyCosts = " daily_costs "
231+ case totalUsage = " total_usage "
232+ }
233+ }
234+
235+ struct DailyCost : Codable {
236+ let timestamp : Double
237+ let lineItems : [ LineItem ]
238+
239+ enum CodingKeys : String , CodingKey {
240+ case timestamp
241+ case lineItems = " line_items "
31242 }
32243}
244+
245+ struct LineItem : Codable {
246+ let name : String
247+ let cost : Double
248+ }
249+
0 commit comments