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
58 changes: 58 additions & 0 deletions opencodex-bar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# OpenCodexBar

[OpenCodex](https://github.com/lidge-jun/opencodex) account, quota, usage, and account-routing control for Noctalia.

## Plugin

| Field | Value |
| --- | --- |
| ID | `wy3z/opencodex-bar` |
| Entries | Bar widget: `usage`; panel: `panel`; service: `service` |

Only `service` contacts OpenCodex. Widget and panel use shared state and never see the credential.

## Requirements

- Noctalia v5, plugin API 24
- OpenCodex 2.31.0, with the Management API enabled
- OpenCodex admin token in the Noctalia process's `OPENCODEX_ADMIN_AUTH_TOKEN` or the file at `admin_token_file` (default `~/.opencodex/admin-api-token`). Env wins.
- `xdg-open` on `PATH` (from `xdg-utils`) for the dashboard button

## Usage

Install from the Noctalia plugin store and add the `usage` widget to a bar. Click the widget, or:

```sh
noctalia msg panel-toggle wy3z/opencodex-bar:panel
```

- Accounts: subscription plans, health, reauth, quota windows, active Codex account selection, and confirmed reset-credit use
- Usage: today, 30-day request grid, provider/model totals, estimated cost

Right-click the widget or use Refresh to force a quota refresh. The link button runs `xdg-open` on `base_url`.

## Settings

| Setting | Scope | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `base_url` | Plugin | `string` | `http://127.0.0.1:10100` | API and dashboard URL. HTTP on loopback only; HTTPS otherwise. |
| `admin_token_file` | Plugin | `file` | `~/.opencodex/admin-api-token` | Token file path. Overridden by `OPENCODEX_ADMIN_AUTH_TOKEN` in Noctalia's environment. |
| `poll_seconds` | Plugin | `int` | `30` | Cached poll interval, 10–300 s. |
| `force_refresh_minutes` | Plugin | `int` | `10` | Quota refresh interval, 5–60 min. |
| `hidden_providers` | Plugin | `string` | empty | Comma-separated ids to hide (`openai`/`codex` are aliases). Does not change OpenCodex routing. |
| `theme_colors` | Plugin | `bool` | `false` | Use Noctalia colours instead of the OpenCodex palette. |
| `show_percentage` | Widget | `bool` | `true` | Mean quota used next to the icon. |
| `icon_source` | Widget | `select` | `bars` | `bars`, `active`, or `fixed`. |
| `glyph` | Widget | `glyph` | `brand-openai` | Used when `icon_source` is `fixed`. |

Disabled OpenCodex providers are hidden the same way. Hidden providers are stripped from every figure. Cached-input and reasoning-output totals are omitted when anything is hidden (OpenCodex does not attribute them).

## Notes

- Network: authenticated Management API requests to `base_url` (`X-OpenCodex-API-Key`). Polling uses `GET`; confirmed account actions use `PUT /api/codex-auth/active` and `POST /api/codex-auth/reset-credits/consume`. Non-loopback HTTP is refused.
- Credential: env, then file. If neither provides a valid token, OpenCodex rejects Management API requests. The credential stays in the service; it is not shown, written, or published to plugin state.
- Files: reads the token file. Writes nothing locally. Account selection and reset-credit use mutate OpenCodex state only after an in-panel confirmation.
- Process: `xdg-open` with the dashboard URL. Nothing else is spawned.
- Daily costs are estimates, not invoices. The grid is request volume, not spend.
- Bar % is the mean of each visible account's busiest quota window.
- Accounts are labelled alias / log label / "Main Account" / OpenCodex id — never email. Subscription plans use the values reported by OpenCodex.
102 changes: 102 additions & 0 deletions opencodex-bar/common.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
--!nonstrict
-- Helpers the bar widget and the panel both need. Kept in one place so the two
-- surfaces cannot drift apart on which mark belongs to a provider or on how an
-- account's usage is derived.

local common = {}

function common.themeColors()
return noctalia.getConfig("theme_colors") == true
end

-- Providers the user has chosen not to see, as a comma-separated list of ids
-- ("anthropic, xai"). A plugin setting cannot enumerate providers it only
-- learns about at runtime, so this is free text; matching is case-insensitive
-- and tolerates the label OpenCodex shows ("OpenAI Codex" -> "openai codex")
-- only insofar as the id is what OpenCodex reports.
--
-- Unlike a provider disabled in OpenCodex itself, this hides the provider from
-- this plugin alone: OpenCodex keeps routing through it.
local hiddenCache, hiddenCacheSource = {}, nil

function common.hiddenProviders()
local raw = noctalia.getConfig("hidden_providers")
if type(raw) ~= "string" then raw = "" end
if raw == hiddenCacheSource then return hiddenCache end

local set = {}
for token in raw:gmatch("[^,%s]+") do
set[token:lower()] = true
end
-- Codex-login usage is filed under either id depending on the endpoint, so
-- naming one has to hide the other.
if set.openai then set.codex = true end
if set.codex then set.openai = true end

hiddenCache, hiddenCacheSource = set, raw
return set
end

function common.isHidden(id)
return common.hiddenProviders()[tostring(id or ""):lower()] == true
end

-- The one test both surfaces use: a provider is shown when OpenCodex has it
-- enabled and the user has not hidden it here.
function common.providerVisible(provider)
return provider.enabled == true and not common.isHidden(provider.id)
end

-- The worst window in a quota, or nil when it reports none.
function common.maxQuota(quota)
local maximum = nil
for _, window in ipairs((quota and quota.windows) or {}) do
if type(window.usedPercent) == "number" and (maximum == nil or window.usedPercent > maximum) then
maximum = window.usedPercent
end
end
return maximum
end

function common.anyAccountQuota(provider)
for _, account in ipairs(provider.accounts or {}) do
if account.quota ~= nil then return true end
end
return false
end

-- An account's own worst window, or its provider's when OpenCodex only reports
-- quota at the provider level (xAI does this).
function common.accountUsed(provider, account, providerHasAccountQuota)
if account.quota ~= nil then return common.maxQuota(account.quota) end
if providerHasAccountQuota then return nil end
return common.maxQuota(provider.quota)
end

-- Tabler glyphs already bundled with Noctalia. Unknown providers deliberately
-- use the generic robot rather than importing artwork from OpenCodex.
local PROVIDER_GLYPHS = {
codex = "brand-openai",
openai = "brand-openai",
grok = "brand-x",
xai = "brand-x",
gemini = "brand-google",
google = "brand-google",
vertex = "brand-google",
copilot = "brand-github",
github = "brand-github",
}

function common.providerGlyph(id)
local key = tostring(id or ""):lower()
local named = PROVIDER_GLYPHS[key]
if named ~= nil then return named end

-- Deployments sometimes suffix ids, for example "openai_work".
for provider, glyph in pairs(PROVIDER_GLYPHS) do
if key:sub(1, #provider) == provider then return glyph end
end
return nil
end

return common
Loading