Skip to content

Commit 26e7b3d

Browse files
committed
initial commit
1 parent 4b1db0f commit 26e7b3d

File tree

3 files changed

+325
-14
lines changed

3 files changed

+325
-14
lines changed

.gitignore

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Xcode
2+
#
3+
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4+
5+
## User settings
6+
xcuserdata/
7+
8+
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9+
*.xcscmblueprint
10+
*.xccheckout
11+
12+
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13+
build/
14+
DerivedData/
15+
*.moved-aside
16+
*.pbxuser
17+
!default.pbxuser
18+
*.mode1v3
19+
!default.mode1v3
20+
*.mode2v3
21+
!default.mode2v3
22+
*.perspectivev3
23+
!default.perspectivev3
24+
25+
## Obj-C/Swift specific
26+
*.hmap
27+
28+
## App packaging
29+
*.ipa
30+
*.dSYM.zip
31+
*.dSYM
32+
33+
## Playgrounds
34+
timeline.xctimeline
35+
playground.xcworkspace
36+
37+
# Swift Package Manager
38+
#
39+
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40+
# Packages/
41+
# Package.pins
42+
# Package.resolved
43+
# *.xcodeproj
44+
#
45+
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46+
# hence it is not needed unless you have added a package configuration file to your project
47+
# .swiftpm
48+
49+
.build/
50+
51+
# CocoaPods
52+
#
53+
# We recommend against adding the Pods directory to your .gitignore. However
54+
# you should judge for yourself, the pros and cons are mentioned at:
55+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56+
#
57+
# Pods/
58+
#
59+
# Add this line if you want to avoid checking in source code from the Xcode workspace
60+
# *.xcworkspace
61+
62+
# Carthage
63+
#
64+
# Add this line if you want to avoid checking in source code from Carthage dependencies.
65+
# Carthage/Checkouts
66+
67+
Carthage/Build/
68+
69+
# Accio dependency management
70+
Dependencies/
71+
.accio/
72+
73+
# fastlane
74+
#
75+
# It is recommended to not store the screenshots in the git repo.
76+
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
77+
# For more information about the recommended setup visit:
78+
# https://docs.fastlane.tools/best-practices/source-control/#source-control
79+
80+
fastlane/report.xml
81+
fastlane/Preview.html
82+
fastlane/screenshots/**/*.png
83+
fastlane/test_output
84+
85+
# Code Injection
86+
#
87+
# After new code Injection tools there's a generated folder /iOSInjectionProject
88+
# https://github.com/johnno1962/injectionforxcode
89+
90+
iOSInjectionProject/
91+
*.xcbkptlist
92+
/TalkGPT/Config.plist

OpenAIAPIUsage/OpenAIAPIUsage.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
<true/>
77
<key>com.apple.security.files.user-selected.read-only</key>
88
<true/>
9+
<key>com.apple.security.network.client</key>
10+
<true/>
911
</dict>
1012
</plist>

OpenAIAPIUsage/OpenAIAPIUsageApp.swift

Lines changed: 231 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,240 @@ import SwiftData
1010

1111
@main
1212
struct 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

Comments
 (0)