Files
hammerspoon/GeminiMonitor.lua
T
2026-05-14 18:59:23 -04:00

110 lines
4.6 KiB
Lua

-- 1. Identify it uniquely (Fresh ID forces macOS to update the hover title)
GeminiMonitorItem = hs.menubar.new(true, "GeminiMonitorV3")
-- 2. Set the hover text (Tooltip)
if GeminiMonitorItem then
GeminiMonitorItem:setTooltip("Gemini Usage Monitor")
end
local JSON_KEY_PATH = os.getenv("HOME") .. "/.hammerspoon/gcp-key.json"
local PROJECT_ID = "gen-lang-client-0842463959"
local UPDATE_INTERVAL = 300
-- 3. Setup the Icon
-- "NSActionTemplate" is the standard 'Gear' icon
local geminiStar = hs.image.imageFromName("NSActionTemplate")
if geminiStar then
-- 'template' allows the icon to switch between black and white for Dark Mode
geminiStar:template(true)
GeminiMonitorItem:setIcon(geminiStar)
end
-- 2026 Flash Preview Pricing
local PRICING_ESTIMATES = {
["Gemini 3 Flash Preview"] = 0.0010,
["Gemini 3.1 Pro"] = 0.0450,
["Gemini 2.0 Ultra"] = 0.1500
}
local selectedModel = "Gemini 3 Flash Preview"
local stats = {
requests = 0,
lastUpdate = "Never"
}
local authToken = nil
-- POPUP ON LOAD
hs.alert.show("GeminiMonitor Loaded", 2)
local function getAccessToken(callback)
local keyFile = io.open(JSON_KEY_PATH, "r")
if not keyFile then return end
local keyData = hs.json.decode(keyFile:read("*all"))
keyFile:close()
local now = os.time()
local header = hs.base64.encode(hs.json.encode({alg="RS256", typ="JWT"}))
local claim = hs.base64.encode(hs.json.encode({iss=keyData.client_email, scope="https://www.googleapis.com/auth/monitoring.read", aud="https://oauth2.googleapis.com/token", exp=now+3600, iat=now}))
local sign_cmd = string.format("echo -n '%s' | openssl dgst -sha256 -sign <(echo '%s') -binary | openssl base64", header.."."..claim, keyData.private_key)
hs.task.new("/bin/bash", function(c, out, e)
local sig = out:gsub("%s+", ""):gsub("/", "_"):gsub("+", "-"):gsub("=", "")
hs.http.asyncPost("https://oauth2.googleapis.com/token", "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion="..header.."."..claim.."."..sig, {["Content-Type"]="application/x-www-form-urlencoded"}, function(s, b)
local res = hs.json.decode(b)
if res and res.access_token then callback(res.access_token) end
end)
end, {"-c", sign_cmd}):start()
end
local function updateUsage()
if not authToken then
getAccessToken(function(token) authToken = token; updateUsage() end)
return
end
local endTime = os.date("!%Y-%m-%dT%H:%M:%SZ")
local startTime = os.date("!%Y-%m-%dT%H:%M:%SZ", os.time() - 86400)
local filter = 'resource.type="consumed_api" AND metric.type="serviceruntime.googleapis.com/api/request_count" AND resource.labels.service="generativelanguage.googleapis.com"'
local url = string.format("https://monitoring.googleapis.com/v3/projects/%s/timeSeries?filter=%s&interval.startTime=%s&interval.endTime=%s",
PROJECT_ID, hs.http.encodeForQuery(filter), startTime, endTime)
hs.http.asyncGet(url, {Authorization = "Bearer "..authToken}, function(status, body)
if status ~= 200 then return end
local data = hs.json.decode(body)
local r = 0
if data.timeSeries then
for _, s in ipairs(data.timeSeries) do
for _, p in ipairs(s.points) do r = r + (tonumber(p.value.int64Value) or 0) end
end
end
stats.requests = r
stats.lastUpdate = os.date("%H:%M")
local estCost = r * PRICING_ESTIMATES[selectedModel]
GeminiMonitorItem:setTitle(string.format(" %d ($%0.3f)", r, estCost))
end)
end
GeminiMonitorItem:setMenu(function()
return {
{ title = "GEMINI FLASH MONITOR", disabled = true },
{ title = "Project: " .. PROJECT_ID, disabled = true },
{ title = "-" },
{ title = "24H Requests: " .. stats.requests },
{ title = string.format("Est. Cost: $%0.4f", stats.requests * PRICING_ESTIMATES[selectedModel]) },
{ title = "-" },
{ title = "ACTIVE MODEL:", disabled = true },
{ title = "Gemini 3 Flash Preview", checked = (selectedModel == "Gemini 3 Flash Preview"), fn = function() selectedModel = "Gemini 3 Flash Preview"; updateUsage() end },
{ title = "Gemini 3.1 Pro", checked = (selectedModel == "Gemini 3.1 Pro"), fn = function() selectedModel = "Gemini 3.1 Pro"; updateUsage() end },
{ title = "-" },
{ title = "Refresh Now", fn = updateUsage },
{ title = "Last Sync: " .. stats.lastUpdate, disabled = true }
}
end)
updateUsage()
if GeminiTimer then GeminiTimer:stop() end
GeminiTimer = hs.timer.doEvery(UPDATE_INTERVAL, updateUsage)