Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ OpenUsage lives in your menu bar and shows you how much of your AI coding subscr
- [**Kiro**](docs/providers/kiro.md) / credits, bonus credits, overages
- [**Kimi Code**](docs/providers/kimi.md) / session, weekly
- [**MiniMax**](docs/providers/minimax.md) / coding plan session
- [**Neuralwatt**](docs/providers/neuralwatt.md) / subscription energy, balance credits
- [**OpenCode Go**](docs/providers/opencode-go.md) / 5h, weekly, monthly spend limits
- [**Windsurf**](docs/providers/windsurf.md) / prompt credits, flex credits
- [**Z.ai**](docs/providers/zai.md) / session, weekly, web searches
Expand Down
71 changes: 71 additions & 0 deletions docs/providers/neuralwatt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Neuralwatt

> Uses the Neuralwatt quota API with a user-provided API key.

## Overview

- **Protocol:** HTTPS (JSON)
- **Endpoint:** `GET https://api.neuralwatt.com/v1/quota`
- **Auth:** `Authorization: Bearer <api_key>`
- **Env var:** `NEURALWATT_API_KEY`

## Authentication

The plugin reads `NEURALWATT_API_KEY` from the environment. If the key is missing, it throws:

- `Neuralwatt API key missing. Set NEURALWATT_API_KEY.`

## Data Source

Request:

```http
GET /v1/quota HTTP/1.1
Host: api.neuralwatt.com
Authorization: Bearer <api_key>
Accept: application/json
User-Agent: OpenUsage
```

Expected payload fields:

- `balance.credits_remaining_usd`, `balance.total_credits_usd`, `balance.credits_used_usd`
- `balance.accounting_method` (e.g. `"energy"`, `"token"`)
- `subscription.plan`, `subscription.status`, `subscription.billing_interval`
- `subscription.current_period_start`, `subscription.current_period_end`
- `subscription.kwh_included`, `subscription.kwh_used`, `subscription.kwh_remaining`
- `subscription.auto_renew`, `subscription.in_overage`

## Usage Mapping

- **Subscription** (overview progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`.
- **Balance** (overview progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`.
- **Method** (detail badge): `accounting_method`, capitalized. Hidden when absent.

The **Subscription** line includes `resetsAt` and `periodDurationMs` from the subscription period dates when available; the **Balance** line does not.

## Output

- **Plan**: from `subscription.plan`, capitalized
- **Subscription** (overview progress line):
- `format`: count with `kWh` suffix
- `used`: `kwh_used`
- `limit`: `kwh_included`
- `resetsAt`: from `current_period_end`
- `periodDurationMs`: `current_period_end` – `current_period_start`
- **Balance** (overview progress line):
- `format`: dollars
- `used`: `credits_used_usd`
- `limit`: `total_credits_usd`
- **Method** (detail badge): capitalized `accounting_method`
- **Status** (overview badge): shown as "No usage data" (gray) when subscription and balance are both absent

## Errors

| Condition | Message |
|---|---|
| Missing API key | `Neuralwatt API key missing. Set NEURALWATT_API_KEY.` |
| HTTP 401/403 | `Invalid API key. Check NEURALWATT_API_KEY.` |
| Non-2xx | `Request failed (HTTP {status}). Try again later.` |
| Network failure | `Request failed. Check your connection.` |
| Unparseable payload | `Response invalid. Try again later.` |
3 changes: 3 additions & 0 deletions plugins/neuralwatt/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
152 changes: 152 additions & 0 deletions plugins/neuralwatt/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
(function () {
var API_KEY_ENV_VARS = ["NEURALWATT_API_KEY"]
var QUOTA_URL = "https://api.neuralwatt.com/v1/quota"

function readNumber(value) {
if (typeof value === "number") return Number.isFinite(value) ? value : null
if (typeof value === "string") {
var parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}

function readString(value) {
if (typeof value !== "string") return null
var trimmed = value.trim()
return trimmed || null
}

function parseDateMs(value) {
if (typeof value === "number") return Number.isFinite(value) ? value : null
if (typeof value === "string") {
var parsed = Date.parse(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}

function parseSubscriptionPeriodMs(sub) {
if (!sub || !sub.current_period_start || !sub.current_period_end) return null
var startMs = parseDateMs(sub.current_period_start)
var endMs = parseDateMs(sub.current_period_end)
if (startMs !== null && endMs !== null && endMs > startMs) return endMs - startMs
return null
}

function loadApiKey(ctx) {
for (var i = 0; i < API_KEY_ENV_VARS.length; i += 1) {
var name = API_KEY_ENV_VARS[i]
var value = null
try {
value = ctx.host.env.get(name)
} catch (e) {
ctx.host.log.warn("env read failed for " + name + ": " + String(e))
}
if (value && typeof value === "string" && value.trim()) {
ctx.host.log.info("api key loaded from " + name)
return { value: value.trim(), source: name }
}
}
return null
}

function probe(ctx) {
var apiKeyInfo = loadApiKey(ctx)
if (!apiKeyInfo) {
throw "Neuralwatt API key missing. Set NEURALWATT_API_KEY."
}

var resp
try {
resp = ctx.util.request({
method: "GET",
url: QUOTA_URL,
headers: {
Authorization: "Bearer " + apiKeyInfo.value,
Accept: "application/json",
"User-Agent": "OpenUsage",
},
timeoutMs: 10000,
})
} catch (e) {
throw "Request failed. Check your connection."
}

if (ctx.util.isAuthStatus(resp.status)) {
throw "Invalid API key. Check NEURALWATT_API_KEY."
}
if (resp.status < 200 || resp.status >= 300) {
throw "Request failed (HTTP " + String(resp.status) + "). Try again later."
}

var data = ctx.util.tryParseJson(resp.bodyText)
if (!data || typeof data !== "object") {
throw "Response invalid. Try again later."
}

var sub = data.subscription && typeof data.subscription === "object" ? data.subscription : null
var balance = data.balance && typeof data.balance === "object" ? data.balance : null
var plan = null
var resetsAt = null
var periodDurationMs = null

if (sub) {
if (typeof sub.plan === "string" && sub.plan) {
plan = sub.plan.charAt(0).toUpperCase() + sub.plan.slice(1)
}
if (sub.current_period_end) {
var endMs = parseDateMs(sub.current_period_end)
resetsAt = endMs !== null ? ctx.util.toIso(endMs) : null
}
periodDurationMs = parseSubscriptionPeriodMs(sub)
}

var lines = []

// Subscription energy line (hidden if no subscription)
if (sub) {
var kwhIncluded = readNumber(sub.kwh_included)
var kwhUsed = readNumber(sub.kwh_used)
if (kwhIncluded !== null && kwhIncluded > 0 && kwhUsed !== null) {
var energyLine = {
label: "Subscription",
used: Math.round(kwhUsed * 10000) / 10000,
limit: Math.round(kwhIncluded * 10000) / 10000,
format: { kind: "count", suffix: "kWh" },
}
if (resetsAt) energyLine.resetsAt = resetsAt
if (periodDurationMs) energyLine.periodDurationMs = periodDurationMs
lines.push(ctx.line.progress(energyLine))
}
}

// Balance line (hidden if total credits is 0)
if (balance) {
var totalCredits = readNumber(balance.total_credits_usd)
var usedCredits = readNumber(balance.credits_used_usd)
if (totalCredits !== null && totalCredits > 0 && usedCredits !== null) {
lines.push(ctx.line.progress({
label: "Balance",
used: Math.round(usedCredits * 100) / 100,
limit: Math.round(totalCredits * 100) / 100,
format: { kind: "dollars" },
}))
}

// Accounting method badge
var method = readString(balance.accounting_method)
if (method) {
lines.push(ctx.line.badge({ label: "Method", text: method.charAt(0).toUpperCase() + method.slice(1) }))
Comment thread
hungps marked this conversation as resolved.
}
}

if (lines.length === 0) {
lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
}

return { plan: plan, lines: lines }
}

globalThis.__openusage_plugin = { id: "neuralwatt", probe: probe }
})()
15 changes: 15 additions & 0 deletions plugins/neuralwatt/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"id": "neuralwatt",
"name": "Neuralwatt",
"version": "0.0.1",
"entry": "plugin.js",
"icon": "icon.svg",
"brandColor": "#D55934",
"lines": [
{ "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 },
{ "type": "progress", "label": "Balance", "scope": "overview" },
{ "type": "badge", "label": "Method", "scope": "detail" },
{ "type": "badge", "label": "Status", "scope": "overview" }
]
}
Loading
Loading