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 +- 2 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/src/lib/components/KeyUsage.svelte (limited to 'packages/frontend/src/lib/components') 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"} -- cgit v1.2.3