-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat: display Anthropic and OpenAI OAuth usage in status dialog and sidebar #6905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
…idebar - Add usage/anthropic.ts: fetch usage from Anthropic OAuth API - Add usage/openai.ts: fetch usage from OpenAI/ChatGPT backend API - Update dialog-status.tsx: show usage bars with reset times - Update sidebar.tsx: show compact usage display with progress bars Shows rate limit utilization percentages and reset times for: - Anthropic: 5-hour and 7-day windows - OpenAI: primary/secondary windows with plan type and credits Related to anomalyco#6298 (plan usage tracking)
|
The following comment was made by an LLM, it may be inaccurate: SummaryI found 1 related PR that may be worth considering: Related PR:
No Direct Duplicates Found:The main PR (#6905) appears to be the only one actively addressing OAuth usage display for Anthropic and OpenAI specifically. The searches did not reveal any other open PRs duplicating this exact feature. Recommendation: The PR is likely ready to proceed, but keep PR #6298 in mind for potential future refactoring to unify the usage tracking patterns across different providers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds real-time OAuth usage display for Anthropic and OpenAI accounts, showing rate limits, utilization percentages, and reset times in both the status dialog (/status) and sidebar. The implementation introduces two new usage modules that fetch data from provider APIs and integrates them into existing UI components with color-coded progress bars.
Key Changes:
- Added usage fetching modules for Anthropic and OpenAI OAuth APIs with Zod validation
- Integrated collapsible usage sections in sidebar and status dialog with visual progress bars
- Implemented utility functions for formatting durations, reset times, and credits display
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.
| File | Description |
|---|---|
| packages/opencode/src/usage/openai.ts | New module for fetching OpenAI usage data with plan type detection, rate limit windows, and credit balance formatting |
| packages/opencode/src/usage/anthropic.ts | New module for fetching Anthropic usage data with 5-hour and 7-day rate limit windows |
| packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | Added collapsible Usage section with compact 10-char progress bars for both providers |
| packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx | Added usage display with 20-char progress bars and UsageBar component |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function formatResetTime(isoString: string | null): string { | ||
| if (!isoString) return "N/A" | ||
| const date = new Date(isoString) | ||
| const now = new Date() | ||
| const diffMs = date.getTime() - now.getTime() | ||
| const diffMins = Math.floor(diffMs / 60000) | ||
| const diffHours = Math.floor(diffMins / 60) | ||
|
|
||
| if (diffHours >= 24) { | ||
| const days = Math.floor(diffHours / 24) | ||
| return `${days}d ${diffHours % 24}h` | ||
| } | ||
| if (diffHours > 0) { | ||
| return `${diffHours}h ${diffMins % 60}m` | ||
| } | ||
| if (diffMins > 0) { | ||
| return `${diffMins}m` | ||
| } | ||
| return "soon" | ||
| } |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formatResetTime function does not handle negative time differences (when the reset time is in the past). This could display incorrect values like negative minutes or hours if the timestamp is stale. Consider adding a check to return a more appropriate message like "expired" or "refreshing" for past timestamps.
|
|
||
| const usageBar = (percent: number) => { | ||
| const width = 10 | ||
| const filled = Math.round((percent / 100) * width) |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usageBar function does not handle negative or values greater than 100. If the API returns invalid percentage values, this could result in incorrect visual representation or runtime errors from negative repeat counts. Consider adding validation to clamp the percent value between 0 and 100.
| const filled = Math.round((percent / 100) * width) | |
| const clampedPercent = Math.max(0, Math.min(100, percent)) | |
| const filled = Math.round((clampedPercent / 100) * width) |
| function UsageBar(props: { percent: number; fg: RGBA; bgMuted: RGBA }) { | ||
| const width = 20 | ||
| const filled = Math.round((props.percent / 100) * width) | ||
| const empty = width - filled | ||
| return ( | ||
| <text> | ||
| <span style={{ fg: props.fg }}>{"█".repeat(filled)}</span> | ||
| <span style={{ fg: props.bgMuted }}>{"░".repeat(empty)}</span> | ||
| </text> | ||
| ) |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UsageBar component does not validate that percent is within the valid range (0-100). If the API returns invalid percentage values, this could result in incorrect visual representation or runtime errors from negative repeat counts. Consider adding validation to clamp the percent value between 0 and 100.
| export function getPlanDisplayName(planType: PlanType): string { | ||
| const names: Record<PlanType, string> = { | ||
| free: "Free", | ||
| plus: "Plus", | ||
| pro: "Pro", | ||
| team: "Team", | ||
| business: "Business", | ||
| enterprise: "Enterprise", | ||
| edu: "Education", | ||
| education: "Education", | ||
| guest: "Guest", | ||
| go: "Go", | ||
| free_workspace: "Free Workspace", | ||
| quorum: "Quorum", | ||
| k12: "K-12", | ||
| } | ||
| return names[planType] || planType | ||
| } | ||
|
|
||
| export async function fetch(): Promise<UsageData | null> { | ||
| const auth = await Auth.get("openai") | ||
| if (!auth || auth.type !== "oauth") { | ||
| return null | ||
| } | ||
|
|
||
| try { | ||
| const response = await globalThis.fetch("https://chatgpt.com/backend-api/wham/usage", { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${auth.access}`, | ||
| Accept: "application/json", | ||
| "User-Agent": "opencode-cli", | ||
| }, | ||
| }) | ||
|
|
||
| if (!response.ok) { | ||
| console.error(`OpenAI usage API error: ${response.status}`) | ||
| return null | ||
| } | ||
|
|
||
| const data = await response.json() | ||
| const parsed = UsageData.safeParse(data) | ||
| if (!parsed.success) { | ||
| console.error("Failed to parse OpenAI usage data:", parsed.error) | ||
| return null | ||
| } | ||
|
|
||
| return parsed.data | ||
| } catch (error) { | ||
| console.error("Failed to fetch OpenAI usage:", error) | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| export function formatWindowDuration(seconds: number): string { | ||
| const minutes = Math.floor(seconds / 60) | ||
| const hours = Math.floor(minutes / 60) | ||
| const days = Math.floor(hours / 24) | ||
|
|
||
| if (days >= 1) { | ||
| return `${days}d` | ||
| } | ||
| if (hours >= 1) { | ||
| return `${hours}h` | ||
| } | ||
| return `${minutes}m` | ||
| } | ||
|
|
||
| export function formatResetTime(unixTimestamp: number): string { | ||
| const date = new Date(unixTimestamp * 1000) | ||
| const now = new Date() | ||
| const diffMs = date.getTime() - now.getTime() | ||
| const diffMins = Math.floor(diffMs / 60000) | ||
| const diffHours = Math.floor(diffMins / 60) | ||
|
|
||
| if (diffHours >= 24) { | ||
| const days = Math.floor(diffHours / 24) | ||
| return `${days}d ${diffHours % 24}h` | ||
| } | ||
| if (diffHours > 0) { | ||
| return `${diffHours}h ${diffMins % 60}m` | ||
| } | ||
| if (diffMins > 0) { | ||
| return `${diffMins}m` | ||
| } | ||
| return "soon" | ||
| } | ||
|
|
||
| export function formatCredits(balance: string | number | null | undefined): string { | ||
| if (balance === null || balance === undefined) { | ||
| return "N/A" | ||
| } | ||
| const num = typeof balance === "string" ? parseFloat(balance) : balance | ||
| return `$${num.toFixed(2)}` | ||
| } |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These utility functions (getPlanDisplayName, formatWindowDuration, formatResetTime, formatCredits) lack documentation. Adding JSDoc comments would help other developers understand their purpose, parameters, and return values, especially since they handle various edge cases and formatting logic.
| Authorization: `Bearer ${auth.access}`, | ||
| Accept: "application/json", | ||
| "User-Agent": "opencode-cli", | ||
| }, |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetch calls do not include a timeout, which could cause the UI to hang indefinitely if the API endpoints are unresponsive. Consider adding a timeout using AbortSignal.timeout() similar to how it's used elsewhere in the codebase (e.g., in provider/models.ts with a 10-second timeout).
| }, | |
| }, | |
| signal: AbortSignal.timeout(10_000), |
| const getUsageColor = (percent: number) => { | ||
| if (percent >= 90) return theme.error | ||
| if (percent >= 70) return theme.warning | ||
| return theme.success | ||
| } |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getUsageColor function logic is duplicated between sidebar.tsx and dialog-status.tsx. Consider extracting this into a shared utility function to maintain consistency and reduce code duplication.
| const [anthropicUsage] = createResource(() => AnthropicUsage.fetch(), { initialValue: null }) | ||
| const [openaiUsage] = createResource(() => OpenAIUsage.fetch(), { initialValue: null }) |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The createResource calls will fetch usage data on every component render without any caching mechanism. This could result in excessive API calls to OpenAI and Anthropic endpoints. Consider implementing caching similar to PR #6298's approach with a 60-second TTL, or adding a refetch mechanism tied to specific user actions rather than continuous polling.
| const [anthropicUsage, { refetch: refetchAnthropicUsage }] = createResource( | ||
| () => AnthropicUsage.fetch(), | ||
| { initialValue: null } | ||
| ) | ||
|
|
||
| const [openaiUsage, { refetch: refetchOpenAIUsage }] = createResource( | ||
| () => OpenAIUsage.fetch(), | ||
| { initialValue: null } | ||
| ) |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The createResource calls will fetch usage data on every component render without any caching mechanism. This could result in excessive API calls to OpenAI and Anthropic endpoints. Consider implementing caching similar to PR #6298's approach with a 60-second TTL, or adding a refresh mechanism that's manually triggered rather than automatic on each render.
| const [anthropicUsage, { refetch: refetchAnthropicUsage }] = createResource( | ||
| () => AnthropicUsage.fetch(), | ||
| { initialValue: null } | ||
| ) | ||
|
|
||
| const [openaiUsage, { refetch: refetchOpenAIUsage }] = createResource( |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refetch functions are destructured but never used. Either implement a refresh mechanism (e.g., a button or periodic refresh) or remove these unused destructured variables to keep the code clean.
| const [anthropicUsage, { refetch: refetchAnthropicUsage }] = createResource( | |
| () => AnthropicUsage.fetch(), | |
| { initialValue: null } | |
| ) | |
| const [openaiUsage, { refetch: refetchOpenAIUsage }] = createResource( | |
| const [anthropicUsage] = createResource( | |
| () => AnthropicUsage.fetch(), | |
| { initialValue: null } | |
| ) | |
| const [openaiUsage] = createResource( |
| export function formatResetTime(unixTimestamp: number): string { | ||
| const date = new Date(unixTimestamp * 1000) | ||
| const now = new Date() | ||
| const diffMs = date.getTime() - now.getTime() | ||
| const diffMins = Math.floor(diffMs / 60000) | ||
| const diffHours = Math.floor(diffMins / 60) | ||
|
|
||
| if (diffHours >= 24) { | ||
| const days = Math.floor(diffHours / 24) | ||
| return `${days}d ${diffHours % 24}h` | ||
| } | ||
| if (diffHours > 0) { | ||
| return `${diffHours}h ${diffMins % 60}m` | ||
| } | ||
| if (diffMins > 0) { | ||
| return `${diffMins}m` | ||
| } | ||
| return "soon" | ||
| } |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formatResetTime function does not handle negative time differences (when the reset time is in the past). This could display incorrect values like negative minutes or hours if the timestamp is stale. Consider adding a check to return a more appropriate message like "expired" or "refreshing" for past timestamps.
- Add negative time handling in formatResetTime (return 'refreshing') - Add clamp for percent values in usage bars (0-100) - Add 10s timeout to fetch calls (AbortSignal.timeout) - Add NaN handling in formatCredits - Extract shared utils (getUsageColor, clampPercent, usageBarString) - Add refetch on message change for sidebar usage display - Remove unused refetch destructuring in dialog-status
Summary
Adds real-time usage display for Anthropic and OpenAI OAuth accounts in both the status dialog (
/status) and the sidebar.Features
Screenshots
Status Dialog (
/statusorCtrl+X S)Sidebar (collapsible Usage section)
Implementation
New Files
src/usage/anthropic.ts: Fetches fromapi.anthropic.com/api/oauth/usagesrc/usage/openai.ts: Fetches fromchatgpt.com/backend-api/wham/usageModified Files
dialog-status.tsx: Added usage sections with progress barssidebar.tsx: Added collapsible Usage sectionAPI Details
GET api.anthropic.com/api/oauth/usageanthropic-beta: oauth-2025-04-20GET chatgpt.com/backend-api/wham/usageRelated
Notes