feat: add ProxyBudgetWidget for LiteLLM/OpenRouter/compatible proxies#367
Open
sagy101 wants to merge 4 commits into
Open
feat: add ProxyBudgetWidget for LiteLLM/OpenRouter/compatible proxies#367sagy101 wants to merge 4 commits into
sagy101 wants to merge 4 commits into
Conversation
Adds the data layer for an upcoming proxy-budget widget that fetches spend-vs-cap from an HTTP proxy (LiteLLM, OpenRouter, Portkey, Helicone, and any other bearer-token JSON endpoint). The widget itself follows in the next commit so this one is reviewable in isolation. src/utils/proxy-budget-fetch.ts (new): - HTTPS GET via Node stdlib (no new deps); AbortController-style timeout via req.setTimeout for the same effect. - Dotted-string JSON path resolver for spend/budget/resetAt fields. - Sanity guard: rejects spend > 2x budget (likely misconfigured path). - Disk cache at ~/.cache/ccstatusline/proxy-budget.json with 60s TTL by default, plus stale fallback up to 10x TTL on transient fetch failure. Lock file at ~/.cache/ccstatusline/proxy-budget.lock prevents concurrent fetches. - Token read from configurable env var (default ANTHROPIC_AUTH_TOKEN); never written to disk cache, never echoed in error paths. - Bearer (default) and x-api-key auth schemes. src/utils/usage-prefetch.ts: - New hasProxyBudgetWidget(lines) and prefetchProxyBudgetIfNeeded(lines) exports. Keeps all prefetch coordination in one orchestrator file. src/types/RenderContext.ts: - Optional proxyBudgetData?: ProxyBudgetData | null field. Widgets read it synchronously after the prefetcher populates it. src/ccstatusline.ts: - Two lines: call prefetchProxyBudgetIfNeeded next to the existing usage prefetch and attach the result to RenderContext. src/utils/__tests__/proxy-budget-fetch.test.ts (new, 13 cases): - LiteLLM happy path, custom JSON paths (OpenRouter shape), missing env, non-2xx, malformed JSON, missing field, sanity guard, timeout, bearer vs x-api-key headers, fresh-cache hit, stale fallback, network down + no cache. All 1267 tests pass (1254 baseline + 13 new). Lint clean.
Adds a 'preset' metadata key for the proxy-budget widget that bundles
the endpoint suffix, JSON paths, and auth scheme for a known proxy.
v1 ships two verified presets:
- litellm: GET ${baseUrl}/key/info -> info.spend / info.max_budget / info.budget_reset_at
- openrouter: GET ${baseUrl}/api/v1/key -> data.usage / data.limit / data.limit_reset
The default (no preset) remains litellm-shaped so existing usage is
unchanged. Users override individual fields per-key as before; the per-
key value wins over the preset default.
The registry is designed for easy extension. Adding a new preset is a
single object entry in PROXY_BUDGET_PRESETS:
newProxy: {
endpoint: '${baseUrl}/...',
spendPath: 'a.b.c',
budgetPath: 'a.b.d',
resetAtPath: 'a.b.e',
authScheme: 'bearer'
}
The exported ProxyBudgetPreset type, the isProxyBudgetPreset runtime
guard, the prefetcher's metadata.preset validator, and the test matrix
all derive from the registry keys via 'keyof typeof'. No other file
needs to change to add a preset.
Two new tests iterate the registry to assert (a) the guard accepts
every key and rejects unknowns, and (b) every entry has a fully-
specified shape. So adding a malformed preset entry fails the suite
immediately.
OpenRouter response shape verified against
https://openrouter.ai/docs/api/api-reference/api-keys/get-current-key
(spend = data.usage, cap = data.limit, reset = data.limit_reset).
All 1272 tests pass (1267 prior + 5 new preset cases). Lint clean.
Adds the user-facing widget that reads the prefetched proxyBudgetData from RenderContext and renders an inline color-tiered indicator. Type string 'proxy-budget'; category 'Usage'; default chalk color 'green'; implements Widget plus the optional getNumericValue() for forward compatibility with the interface's threshold hook. Display: - Default format 'spend-percent' => '$5.00/$100.00 (5%)' - 'percent' => '5%' - 'spend' => '$5.00' Color tiers reuse chalk.green / chalk.yellow / chalk.red embedded in the returned string. The rendering pipeline at src/utils/renderer.ts:756 wraps with its own outer SGR but the embedded codes survive intact — same mechanism that already works for any colored content. Default thresholds are warningThreshold=80 and criticalThreshold=95, both overridable per-instance via metadata. Out-of-range values fall back silently. Configuration is read entirely from item.metadata (string-keyed) for v1 — no custom Ink editor needed. Matches the early CustomCommand pattern. The editor's modifierText shows the preset name or endpoint host so users can tell instances apart at a glance. Registration: - src/widgets/index.ts: export ProxyBudgetWidget - src/utils/widget-manifest.ts: manifest entry 'proxy-budget' in the Usage cluster Tests (16 cases in src/widgets/__tests__/ProxyBudget.test.ts): - null data returns null (segment hides gracefully) - green / yellow / red at default tier boundaries - custom warningThreshold / criticalThreshold from metadata - format=percent / format=spend - item.rawValue strips the 'Budget:' label - isPreview returns hardcoded example with and without rawValue - invalid format and out-of-range thresholds fall back to defaults - getEditorDisplay shows preset hint - getNumericValue returns percentage or null - supportsRawValue and supportsColors both true All 1288 tests pass (1272 prior + 16 new widget). Lint clean.
Adds a section in docs/USAGE.md mirroring the existing widget documentation pattern. Covers: - the two ships-with-the-PR presets (litellm default, openrouter verified against openrouter.ai's /api/v1/key) - the full metadata table with defaults - an example covering both the Claude-Code-routed setup (no env config needed) and OpenRouter direct - explicit 'why a native widget vs Custom Command?' rationale to pre-empt the reviewer question - an 'Out of scope (v1)' note for cloud-native cost APIs (Bedrock SigV4, Vertex JWT, Azure RBAC) so users aren't surprised Also adds a one-line entry in the 'Available Widgets / Tokens, Usage & Context' bulleted index so the new widget appears in the at-a-glance catalog.
4f7a07b to
ec28376
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new opt-in
proxy-budgetwidget that displays spend-vs-cap from a LiteLLM-compatible HTTP proxy, with green/yellow/red threshold tiers. Ships with two verified presets out of the box (LiteLLM and OpenRouter) plus full path-driven configuration for any other bearer-token JSON endpoint (Portkey, Helicone, custom proxies). Defaults to Claude Code's existingANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKENenv vars so the typical setup needs zero config.Happy to rename, restructure, or close if scope doesn't fit. The implementation is included so the design is concrete to react to.
Display
Live render with the widget at the end of line 2 (
Budget: $616.72/$1000.00 (62%)in green):Format options via
metadata.format:spend-percent(default)Budget: $5.00/$100.00 (5%)percentBudget: 5%spendBudget: $5.00Color tiers (default 80% warning / 95% critical, configurable):
< warningwarning..critical>= criticalPresets
Two verified presets ship in this PR. Both auth via
Authorization: Bearer <token>from the env var named bytokenEnv.litellm(default)${baseUrl}/key/infoinfo.spendinfo.max_budgetinfo.budget_reset_atopenrouter${baseUrl}/api/v1/keydata.usagedata.limitdata.limit_resetThe presets live in
PROXY_BUDGET_PRESETSinsrc/utils/proxy-budget-fetch.ts. Adding a new one is a single object entry — the exportedProxyBudgetPresettype, theisProxyBudgetPresetruntime guard, the metadata validator inusage-prefetch.ts, and the test matrix all derive from the registry keys. A test iterates the registry to assert every entry has a fully-specified shape, so adding a malformed preset fails the suite immediately.OpenRouter shape verified against openrouter.ai/docs/api/api-reference/api-keys/get-current-key.
Implementation notes
prefetchProxyBudgetIfNeededtosrc/utils/usage-prefetch.ts, called once per render insrc/ccstatusline.ts(mirrors the existingprefetchUsageDataIfNeededpattern for Anthropic usage). Result attached toRenderContext.proxyBudgetData; the widget reads synchronously.src/utils/proxy-budget-fetch.tsuses Node stdlibhttps(no new deps). Caches at~/.cache/ccstatusline/proxy-budget.jsonwith default 60s TTL, plus stale-cache fallback up to 10× TTL on transient failures. Lock file pattern adapted fromsrc/utils/usage-fetch.ts. Sanity guard (spend > 2 × budget) returns null to suppress display when the proxy returns nonsense.WidgetItem.metadata: Record<string, string>(preset, endpoint, baseUrlEnv, tokenEnv, authScheme, JSON paths, thresholds, cacheTtlSec, timeoutMs, format). v1 ships without a custom Ink editor — users configure in JSON. Matches earlycustom-commandbehavior; arenderEditorcan be added in a follow-up if requested.render()returns a chalk-wrapped string (chalk.green/yellow/red). The renderer atsrc/utils/renderer.ts:756wraps with its own outer SGR but the embedded codes survive intact — same mechanism that already works for any colored content. No changes needed in the renderer.getNumericValuereturning the percentage so future threshold-aware features in the renderer can consume it without re-implementing the widget.tokenEnv(defaultANTHROPIC_AUTH_TOKEN) selects the variable name; the value never touchessettings.json, the disk cache, error logs, or anywhere else.ANTHROPIC_BASE_URLandANTHROPIC_AUTH_TOKENare the env vars Claude Code itself sets when routing through any proxy. The typical setup needs nothing in metadata beyond"enabled": true.Why a native widget vs
CustomCommand?The existing
CustomCommandwidget can run a shell script that hits the same endpoint, but:context.proxyBudgetData.getNumericValueall become script bookkeeping instead of native widget features.The native widget pays a one-time integration cost for repeated correctness.
Files changed
src/utils/proxy-budget-fetch.ts(new) — fetch, cache, lock, sanity guard, preset registry, dotted JSON path resolversrc/utils/usage-prefetch.ts— newhasProxyBudgetWidget/prefetchProxyBudgetIfNeededexportssrc/types/RenderContext.ts— newproxyBudgetData?fieldsrc/ccstatusline.ts— two lines: call prefetch, attach to contextsrc/widgets/ProxyBudget.ts(new) — the widget classsrc/widgets/index.ts— re-exportsrc/utils/widget-manifest.ts— manifest entryproxy-budgetin Usage clustersrc/utils/__tests__/proxy-budget-fetch.test.ts(new) — 18 cases (LiteLLM happy path, OpenRouter preset, custom paths, missing env, non-2xx, malformed JSON, missing field, sanity guard, timeout, bearer/x-api-key headers, fresh/stale cache, network down, registry guard + shape)src/widgets/__tests__/ProxyBudget.test.ts(new) — 16 cases (null data, tier boundaries, custom thresholds, format variants, raw mode, preview mode, invalid format/threshold fallback, editor display, getNumericValue, supportsRawValue/supportsColors)docs/USAGE.md— new "Proxy Budget Widget" section under existing widget docsTest plan
bun tsc --noEmitcleanbun run lintclean (eslint, 0 warnings)bun test— 1288 / 1288 passing (1254 baseline + 34 new)bun run buildproducesdist/ccstatusline.jscleanlyBudget: $X.XX/$Y.YY (NN%)in green; cache written to~/.cache/ccstatusline/proxy-budget.jsonhttps://10.255.255.1): widget hides, lock file cleaned upScope notes
Out of scope for v1: cloud-native cost APIs that don't fit the bearer-token-JSON pattern (AWS Bedrock SigV4 + Cost Explorer, Vertex AI service-account JWT + Cloud Billing, Azure RBAC + Cost Management). Documented in
docs/USAGE.mdunder the widget's "Out of scope" note. Future: separate provider-specific widgets if there's demand.