From c0c55ed7c03188f264ff2c27b9982f2d57d092cc Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 20 May 2026 22:32:41 +0900 Subject: feat: key usage panel — display usage data for Claude, OpenCode, and Copilot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 'Key Usage' to the sidebar dropdown with per-provider live usage: - Claude: 5-hour and weekly utilization bars with reset timestamps (normalizes Anthropic's 0-100% API response to 0-1 internally) - OpenCode: Scrapes usage from workspace page via OPENCODE_COOKIE session cookie, mapping key IDs to OPENCODE_WS1_ID/OPENCODE_WS2_ID - Copilot: Fetches from api.github.com/copilot_internal/user with entitlement/remaining/quota reset tracking New files: opencode.ts, copilot.ts (usage fetchers), KeyUsage.svelte New route: GET /models/key-usage?keyId=X dispatches by provider --- .../frontend/src/lib/components/KeyUsage.svelte | 282 +++++++++++++++++++++ .../src/lib/components/SidebarPanel.svelte | 7 +- packages/frontend/src/lib/types.ts | 32 +++ 3 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/src/lib/components/KeyUsage.svelte (limited to 'packages/frontend') diff --git a/packages/frontend/src/lib/components/KeyUsage.svelte b/packages/frontend/src/lib/components/KeyUsage.svelte new file mode 100644 index 0000000..313c183 --- /dev/null +++ b/packages/frontend/src/lib/components/KeyUsage.svelte @@ -0,0 +1,282 @@ + + +
+ + {#if keys.length > 0} + + {:else} +

No keys available.

+ {/if} + + + {#if loading} +
+ + Loading usage data... +
+ {/if} + + + {#if error} + + {/if} + + + {#if !loading && usageData} + {#if !hasAnyData} +

No usage data available for this key.

+ + {:else if usageData.provider === "anthropic"} +
+

Claude Usage

+ + {#if hasBucketData(usageData.fiveHour)} + {@const bucket = usageData.fiveHour!} + {@const util = bucket.utilization ?? 0} + {@const pct = Math.round(util * 100)} +
+

5-Hour Window

+ +
+ {pct}% + {#if bucket.resetsAt} + Resets: {formatDate(bucket.resetsAt)} + {/if} +
+
+ {/if} + + {#if hasBucketData(usageData.sevenDay)} + {@const bucket = usageData.sevenDay!} + {@const util = bucket.utilization ?? 0} + {@const pct = Math.round(util * 100)} +
+

Weekly (7-Day)

+ +
+ {pct}% + {#if bucket.resetsAt} + Resets: {formatDate(bucket.resetsAt)} + {/if} +
+
+ {/if} +
+ + {:else if usageData.provider === "opencode-go"} +
+

OpenCode Usage

+ + {#if usageData.unavailable} +

+ OpenCode does not expose usage data via API. +

+ {#if usageData.limits} +
+

Rate Limits

+
    + {#if usageData.limits.fiveHour} +
  • 5-Hour: {usageData.limits.fiveHour}
  • + {/if} + {#if usageData.limits.weekly} +
  • Weekly: {usageData.limits.weekly}
  • + {/if} + {#if usageData.limits.monthly} +
  • Monthly: {usageData.limits.monthly}
  • + {/if} +
+
+ {/if} + {#if usageData.consoleUrl} + + View usage in console + + {/if} + {:else} + + {#if hasBucketData(usageData.fiveHour)} + {@const bucket = usageData.fiveHour!} + {@const util = bucket.utilization ?? 0} + {@const pct = Math.round(util * 100)} +
+

5-Hour Window

+ +
+ {pct}% + {#if bucket.resetsAt} + Resets: {formatDate(bucket.resetsAt)} + {/if} +
+
+ {/if} + + {#if hasBucketData(usageData.weekly)} + {@const bucket = usageData.weekly!} + {@const util = bucket.utilization ?? 0} + {@const pct = Math.round(util * 100)} +
+

Weekly

+ +
+ {pct}% + {#if bucket.resetsAt} + Resets: {formatDate(bucket.resetsAt)} + {/if} +
+
+ {/if} + + {#if hasBucketData(usageData.monthly)} + {@const bucket = usageData.monthly!} + {@const util = bucket.utilization ?? 0} + {@const pct = Math.round(util * 100)} +
+

Monthly

+ +
+ {pct}% + {#if bucket.resetsAt} + Resets: {formatDate(bucket.resetsAt)} + {/if} +
+
+ {/if} + {/if} +
+ + {:else if usageData.provider === "github-copilot"} + {@const pct = Math.round(usageData.percentUsed ?? 0)} +
+

Copilot Usage

+
+ +
+ {#if usageData.tokensConsumed !== undefined && usageData.tokensRemaining !== undefined} + + {usageData.tokensConsumed.toLocaleString()} / {(usageData.tokensConsumed + usageData.tokensRemaining).toLocaleString()} ({pct}%) + + {:else} + {pct}% + {/if} + {#if usageData.resetAt} + Resets: {formatDate(usageData.resetAt)} + {/if} +
+
+
+ {/if} + {/if} +
diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index fe92486..2184818 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -4,6 +4,7 @@ import ConfigPanel from "./ConfigPanel.svelte"; import SkillsBrowser from "./SkillsBrowser.svelte"; import PermissionLog from "./PermissionLog.svelte"; + import KeyUsage from "./KeyUsage.svelte"; import type { TaskItem, LogEntry, KeyInfo, ModelInfo } from "../types.js"; const { @@ -24,7 +25,7 @@ let selected = $state("Tasks"); - const options = ["Model Status", "Tasks", "Config", "Skills", "Permission Log"]; + const options = ["Key Usage", "Model Status", "Tasks", "Config", "Skills", "Permission Log"];
@@ -38,7 +39,9 @@
- {#if selected === "Model Status"} + {#if selected === "Key Usage"} + + {:else if selected === "Model Status"} {:else if selected === "Tasks"} diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index 9c83eac..e7ff4ba 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -108,3 +108,35 @@ export interface LogEntry { timestamp: string; description: string; } + +export interface UsageBucket { + utilization?: number; + resetsAt?: number; +} + +export interface ClaudeUsageData { + provider: "anthropic"; + fiveHour?: UsageBucket; + sevenDay?: UsageBucket; +} + +export interface OpencodeUsageData { + provider: "opencode-go"; + unavailable?: boolean; + consoleUrl?: string; + limits?: { fiveHour?: string; weekly?: string; monthly?: string }; + fiveHour?: UsageBucket; + weekly?: UsageBucket; + monthly?: UsageBucket; +} + +export interface CopilotUsageData { + provider: "github-copilot"; + tokensConsumed?: number; + tokensRemaining?: number; + percentUsed?: number; + resetAt?: number; + plan?: string; +} + +export type KeyUsageData = ClaudeUsageData | OpencodeUsageData | CopilotUsageData; -- cgit v1.2.3