Add Fireworks balance usage panel (#6488)

* Add a Fireworks balance collector and teach the agents panel prepaid ledgers

The omarchy-agent-usage-fireworks collector reads serverless token usage
from the Fireworks billing API, grouped by day and model for the last 30
days, and reshapes it into the shared record contract. Fireworks does not
expose its prepaid ledger through the documented API, so the record carries
an estimated balance instead of rate limits: credits configured in
~/.config/omarchy/agents/fireworks.json minus rated account costs since the
funding date. Credentials come from FIREWORKS_API_KEY/FIREWORKS_ACCOUNT_ID,
the auth.ini that firectl set-api-key writes, or — last, so an explicit
login wins — the key opencode stores for its fireworks-ai provider.

The panel gains two generic capabilities any agent record can use: a
balance object draws a BALANCE section — remaining credit, a fuel-gauge
meter that drains toward empty and lights the bar alarm below 10%, and
funded-versus-spent detail — and hasPromptStats: false keeps prompt and
session counts out of today's tooltip for agents whose billing API only
ever reports tokens, on this machine and through synced snapshots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Feed Claude and Codex usage from pi, omp, and opencode sessions

A subscription burned entirely through another coding agent leaves no
native Claude Code transcripts and no Codex session files, so the panel
showed nothing for it. pi and omp write compatible JSONL sessions, and
opencode records per-message provider, model, and token usage in its
message database; the claude and codex collectors now scan all three —
filtered to Anthropic and OpenAI providers respectively — and merge those
numbers into their local stats. Fireworks stays out on purpose: its billing
API already sees that traffic server-side, and a local scan would count the
same tokens twice.

The collector tests pin XDG_DATA_HOME so a developer's real opencode
history cannot leak into fixture runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-07 23:49:43 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b85ae70ebd
commit 77cf58ccfe
11 changed files with 1478 additions and 94 deletions
+98 -5
View File
@@ -39,7 +39,12 @@ Panel {
readonly property var limits: limitWindows(provider)
readonly property var models: modelRows(provider)
readonly property var headline: bindingWindow(provider)
readonly property bool alarming: !!headline && headline.percent >= 0.9
readonly property var balance: provider ? (provider.balance || null) : null
// A prepaid account runs low the way a subscription window fills up: the
// last 10% of the funded credits lights the same alarm.
readonly property bool balanceAlarming: !!balance && balance.funded > 0
&& balance.remaining / balance.funded <= 0.1
readonly property bool alarming: (!!headline && headline.percent >= 0.9) || balanceAlarming
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
function alpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a) }
@@ -134,6 +139,32 @@ Panel {
return Math.max(1, minutes) + "m"
}
// ---------------------------------------------------------------- balance
//
// Prepaid agents report a credit ledger instead of rate-limit windows: the
// record's balance object carries remaining, funded, and spent amounts.
function currencyPrefix(currency) {
var code = String(currency || "USD").toUpperCase()
if (code === "USD") return "$"
if (code === "EUR") return "€"
if (code === "GBP") return "£"
return code + " "
}
function formatMoney(value, currency) {
var amount = Number(value)
if (!isFinite(amount)) amount = 0
return currencyPrefix(currency) + amount.toFixed(2)
}
function balanceDetailText(b) {
if (!b || !(b.funded > 0)) return ""
var text = formatMoney(b.spent, b.currency) + " spent of " + formatMoney(b.funded, b.currency) + " funded"
if (b.estimated) text += " · estimated"
return text
}
// ---------------------------------------------------------------- content
// The plan you pay for, under the name of the tool it pays for. Limits live
@@ -174,8 +205,9 @@ Panel {
: dayName(day.date) + " " + (parsed.getMonth() + 1) + "/" + parsed.getDate()
var text = label + " · " + usage.formatTokenCount(Number(day.messageCount || 0)) + " tokens"
// Prompt and session counts only exist for today, so they ride along here
// instead of taking a section of their own.
if (today && provider)
// instead of taking a section of their own. Billing-API agents never
// count prompts, and "0 prompts" would read as a quiet day, not a gap.
if (today && provider && provider.hasPromptStats !== false)
text += " · " + Number(provider.todayPrompts || 0) + " prompts · "
+ Number(provider.todaySessions || 0) + " sessions"
return text
@@ -476,12 +508,73 @@ Panel {
}
}
// ---------- Limits ----------
// ---------- Balance / limits ----------
PanelSeparator {
visible: limitsSection.visible
visible: balanceSection.visible || limitsSection.visible
foreground: root.foreground
}
Column {
id: balanceSection
visible: !!root.balance
width: parent.width
spacing: Style.space(10)
// The meter shows what is left, not what is used: a prepaid
// account drains toward empty rather than filling toward a cap.
readonly property real ratio: root.balance && root.balance.funded > 0
? root.clamp(root.balance.remaining / root.balance.funded, 0, 1)
: -1
PanelSectionHeader {
width: parent.width
text: "BALANCE"
foreground: root.foreground
fontFamily: root.fontFamily
}
Item {
width: parent.width
implicitHeight: Math.max(balanceLabel.implicitHeight, balanceValue.implicitHeight)
Text {
id: balanceLabel
text: "Prepaid credits"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.body
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: balanceValue
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
color: root.balanceAlarming ? root.urgent : root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.caption
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
}
Meter {
visible: balanceSection.ratio >= 0
width: parent.width
value: balanceSection.ratio
alarming: root.balanceAlarming
}
Text {
visible: text !== ""
width: parent.width
text: root.balanceDetailText(root.balance)
color: root.dim
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
}
Column {
id: limitsSection
visible: root.limits.length > 0