You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GitHubCopilotUsageProvider requests a non-existent endpoint, https://api.github.com/copilot/billing/usage, which returns 404. The provider treats 404 as "no data" and returns an empty metrics list. UsageMetricsService then sees zero metrics and skips registering the account, so the account is never added to UsageMetrics.Accounts, UsageTrackerViewModel.TopRightLabel stays null, and the toolbar usage indicator in MainWindow never appears. This happens even though a valid GitHub user-account entity exists (provider: "https://github.com", user-name: "JoshuaRowePhantom").
Observed runtime log warnings:
2026-07-29T00:01:12.864Z [Warning] Phantom.Workspaces.Services.UsageProviders.GitHubCopilotUsageProvider - GitHub Copilot usage provider returned 404 for https://api.github.com/copilot/billing/usage; returning empty metrics.
2026-07-29T00:01:12.864Z [Warning] Phantom.Workspaces.Services.UsageMetricsService - Usage metrics empty for account JoshuaRowePhantom (https://github.com/copilot); skipping account registration.
Fix: call the correct enhanced billing usage endpoint GET /users/{username}/settings/billing/usage (org variant GET /organizations/{org}/settings/billing/usage), building {username} from the discovered account, and parse the documented usageItems[] schema, mapping the Copilot line items into the metrics model.
Related to #1041.
Root Cause
1. The provider calls a URL that does not exist → 404
"https://api.github.com/copilot/billing/usage"// no {user}/{org} scope segment → 404
This is the request target (built in SendRequestAsync, used by GetMetricsAsync). There is no user/org scope segment, so GitHub returns 404 Not Found for a normal personal token.
The 404 is handled as "empty" — GitHubCopilotUsageProvider.cs:73-80:
if(response.StatusCode==HttpStatusCode.NotFound){this.logger.LogWarning("GitHub Copilot usage provider returned {StatusCode} for {Endpoint}; returning empty metrics.",(int)response.StatusCode,"https://api.github.com/copilot/billing/usage");return[];// 404 → empty list}
This warning is the exact first line in the observed log. (401 is retried once after a token refresh then throws — cs:59-71; other non-success statuses log an error and throw via EnsureSuccessStatusCode() — cs:82-90.)
Note: GetMetricsAsync(UsageAccount account, CancellationToken) (cs:52-54) receives the account but never uses it — the URL is a constant, so the username needed for the correct path is available (UsageAccount.UserName, features/Phantom.Workspaces/Models/UsageMetrics.cs:85) but ignored.
2. Empty metrics → account registration is skipped
varhasMetrics=metrics.Count>0;varisCurrentlyVisible=this.usageMetrics.Accounts.Contains(account);if(hasMetrics){awaitthis.usageMetrics.MutateAsync(async()=>{account.Metrics.Clear();foreach(varmetricinmetrics){account.Metrics.Add(metric);}if(!isCurrentlyVisible){this.usageMetrics.Accounts.Add(account);}// only reached when hasMetricsawaitTask.CompletedTask;}).ConfigureAwait(false);
...}else{this.logger.LogWarning("Usage metrics empty for account {UserName} ({Provider}); skipping account registration.",// cs:266-269discovered.UserName,discovered.ProviderUri);// account is NOT added (removed if previously visible)}
This warning is the exact second line in the observed log. The account identity in the message comes from the discovered user-account entity: UserName from the entity's user-name property and ProviderUri from its provider property (discovery at UsageMetricsService.cs:128-194; host-based provider routing at cs:54-59, cs:173-189). Discovery and routing are correct (the #1041 fixes landed); the only remaining break is that the Copilot provider returns nothing.
Net mechanism
Account entity exists → discovered and routed to the Copilot provider → provider requests the unscoped/copilot/billing/usage → 404 → empty list (GitHubCopilotUsageProvider.cs:100, 73-80) → hasMetrics == false → account never added to UsageMetrics.Accounts (UsageMetricsService.cs:264-269) → TopRightLabel stays null → toolbar indicator hidden.
Path:GET /users/{username}/settings/billing/usage (verified live — returns 200 with real Copilot usage for JoshuaRowePhantom).
Query params (optional):year, month, day — omit to default to the current year/month; add year/month if the indicator should show the current billing period only.
Auth / scopes: fine-grained PAT (or GitHub App user token) with "Plan" user permissions (read). Endpoint is only available to accounts on the enhanced billing platform. Existing token resolution (GitHubAuthTokenResolver) is unchanged.
Headers:Accept: application/vnd.github+json, X-GitHub-Api-Version: 2026-03-10 (docs recommend this version for the enhanced-billing endpoints).
2. Support the organization variant (if org accounts are needed)
For an org-owned account, use GET /organizations/{org}/settings/billing/usage, which requires "Administration" organization permissions (read). Select user- vs org-path based on the discovered account's identity (personal handle vs org). If only personal Copilot usage is tracked, the user endpoint alone is sufficient.
3. Parse the usageItems[] schema and map Copilot line items
The response is no longerseat_breakdown / total_billed_amount. Replace the parser (cs:112-160) to read usageItems[]. Verified live shape:
Filter product == "copilot" (case-insensitive) to isolate Copilot usage from actions / models items.
Group/aggregate the Copilot SKUs (e.g. Copilot Premium Request, Copilot AI Credits) into UsageMetric entries — set Title from the SKU, QuantityUsed/Unit from quantity/unitType, and (optionally) a cost metric from netAmount. Keep the existing UsageMetric presentation-format fields.
Stamp LastUpdatedAt from the injected TimeProvider as today.
4. Distinguish genuine errors from "empty" and keep graceful handling
The current 404→empty behavior masked the real bug (the URL was simply wrong). After pointing at the correct endpoint:
A 404 on the correct path is a genuine "not on enhanced billing / no such user" condition — keep returning empty but log at Warning with the actual URL.
A 403 (insufficient Plan permission) should be logged distinctly (permission problem, not "no usage").
A 200 with zero Copilot items is legitimately empty.
Consider (separately) decoupling GUI visibility from a successful fetch so the indicator can show a neutral/pending state — tracked as background below, not required for this fix.
5. Diagnosability
Log lines for the 404/403/empty cases already exist; ensure they carry the real resolved URL and the account handle. (#1092 / #1086 add the file-logging facility that surfaces this chain from logs.)
Style/class names follow the existing GitHubCopilotUsageProviderTests, UsageMetricsServiceTests, UsageTrackerViewModelTests, and MainWindowUsageTrackerTests suites (features/Phantom.Workspaces.Tests). Tests capture the outgoing request via the existing RequestCapturingHandler / StubHandler fakes and assert on the request URI.
Show the account even with zero/pending usage (Option 2, still valid as hardening). Decoupling GUI visibility from a successful metric fetch — adding the account to UsageMetrics.Accounts regardless of hasMetrics and letting RecomputeTopRightLabel show a neutral label — would make the indicator resilient to any single provider failure. Useful defense-in-depth, but with the correct endpoint the provider now returns real metrics, so this is optional.
Summary
GitHubCopilotUsageProviderrequests a non-existent endpoint,https://api.github.com/copilot/billing/usage, which returns 404. The provider treats 404 as "no data" and returns an empty metrics list.UsageMetricsServicethen sees zero metrics and skips registering the account, so the account is never added toUsageMetrics.Accounts,UsageTrackerViewModel.TopRightLabelstaysnull, and the toolbar usage indicator inMainWindownever appears. This happens even though a valid GitHubuser-accountentity exists (provider: "https://github.com",user-name: "JoshuaRowePhantom").Observed runtime log warnings:
Fix: call the correct enhanced billing usage endpoint
GET /users/{username}/settings/billing/usage(org variantGET /organizations/{org}/settings/billing/usage), building{username}from the discovered account, and parse the documentedusageItems[]schema, mapping the Copilot line items into the metrics model.Related to #1041.Root Cause
1. The provider calls a URL that does not exist → 404
features/Phantom.Workspaces/Services/UsageProviders/GitHubCopilotUsageProvider.cs:100This is the request target (built in
SendRequestAsync, used byGetMetricsAsync). There is no user/org scope segment, so GitHub returns 404 Not Found for a normal personal token.The 404 is handled as "empty" —
GitHubCopilotUsageProvider.cs:73-80:This warning is the exact first line in the observed log. (401 is retried once after a token refresh then throws —
cs:59-71; other non-success statuses log an error and throw viaEnsureSuccessStatusCode()—cs:82-90.)Note:
GetMetricsAsync(UsageAccount account, CancellationToken)(cs:52-54) receives the account but never uses it — the URL is a constant, so the username needed for the correct path is available (UsageAccount.UserName,features/Phantom.Workspaces/Models/UsageMetrics.cs:85) but ignored.2. Empty metrics → account registration is skipped
features/Phantom.Workspaces/Services/UsageMetricsService.cs:234-280(RefreshAccountAsync):This warning is the exact second line in the observed log. The account identity in the message comes from the discovered
user-accountentity:UserNamefrom the entity'suser-nameproperty andProviderUrifrom itsproviderproperty (discovery atUsageMetricsService.cs:128-194; host-based provider routing atcs:54-59,cs:173-189). Discovery and routing are correct (the #1041 fixes landed); the only remaining break is that the Copilot provider returns nothing.Net mechanism
Account entity exists → discovered and routed to the Copilot provider → provider requests the unscoped
/copilot/billing/usage→ 404 → empty list (GitHubCopilotUsageProvider.cs:100, 73-80) →hasMetrics == false→ account never added toUsageMetrics.Accounts(UsageMetricsService.cs:264-269) →TopRightLabelstaysnull→ toolbar indicator hidden.Affected Files
Phantom.Workspaces/Services/UsageProviders/GitHubCopilotUsageProvider.csseat_breakdownJSON shape; ignoresaccountPhantom.Workspaces/Services/UsageMetricsService.csPhantom.Workspaces/Models/UsageMetrics.csUsageMetricandUsageAccountmodels;UsageAccount.UserName(line 85) supplies the path segmentPhantom.Workspaces.Llm.Core/GitHubAuthTokenResolver.csGITHUB_TOKENenv var, elsegh auth token)Design / Fix
1. Request the correct, scoped endpoint
Change the request URL from the unscoped constant to the documented user billing usage endpoint, building the username from the account:
GET /users/{username}/settings/billing/usage(verified live — returns 200 with real Copilot usage forJoshuaRowePhantom).year,month,day— omit to default to the current year/month; addyear/monthif the indicator should show the current billing period only.GitHubAuthTokenResolver) is unchanged.Accept: application/vnd.github+json,X-GitHub-Api-Version: 2026-03-10(docs recommend this version for the enhanced-billing endpoints).2. Support the organization variant (if org accounts are needed)
For an org-owned account, use
GET /organizations/{org}/settings/billing/usage, which requires "Administration" organization permissions (read). Select user- vs org-path based on the discovered account's identity (personal handle vs org). If only personal Copilot usage is tracked, the user endpoint alone is sufficient.3. Parse the
usageItems[]schema and map Copilot line itemsThe response is no longer
seat_breakdown/total_billed_amount. Replace the parser (cs:112-160) to readusageItems[]. Verified live shape:{ "usageItems": [ { "date": "2026-07-01T00:00:00Z", "product": "copilot", "sku": "Copilot AI Credits", "quantity": 395199.59, "unitType": "AICredits", "pricePerUnit": 0.01, "grossAmount": 3951.99, "discountAmount": 197.41, "netAmount": 3754.58, "repositoryName": "" }, { "date": "2026-05-01T00:00:00Z", "product": "copilot", "sku": "Copilot Premium Request", "quantity": 1244, "unitType": "Requests", "pricePerUnit": 0.04, "grossAmount": 49.76, "discountAmount": 49.76, "netAmount": 0.0, "repositoryName": "" } ] }Per-item fields:
date,product,sku,quantity,unitType,pricePerUnit,grossAmount,discountAmount,netAmount,repositoryName. Mapping guidance:product == "copilot"(case-insensitive) to isolate Copilot usage fromactions/modelsitems.Copilot Premium Request,Copilot AI Credits) intoUsageMetricentries — setTitlefrom the SKU,QuantityUsed/Unitfromquantity/unitType, and (optionally) a cost metric fromnetAmount. Keep the existingUsageMetricpresentation-format fields.LastUpdatedAtfrom the injectedTimeProvideras today.4. Distinguish genuine errors from "empty" and keep graceful handling
The current 404→empty behavior masked the real bug (the URL was simply wrong). After pointing at the correct endpoint:
Planpermission) should be logged distinctly (permission problem, not "no usage").Consider (separately) decoupling GUI visibility from a successful fetch so the indicator can show a neutral/pending state — tracked as background below, not required for this fix.
5. Diagnosability
Log lines for the 404/403/empty cases already exist; ensure they carry the real resolved URL and the account handle. (
#1092/#1086add the file-logging facility that surfaces this chain from logs.)Reference: GitHub REST billing usage docs — https://docs.github.com/en/rest/billing/usage?apiVersion=2026-03-10
Expected Tests
Style/class names follow the existing
GitHubCopilotUsageProviderTests,UsageMetricsServiceTests,UsageTrackerViewModelTests, andMainWindowUsageTrackerTestssuites (features/Phantom.Workspaces.Tests). Tests capture the outgoing request via the existingRequestCapturingHandler/StubHandlerfakes and assert on the request URI.GetMetricsAsync_UsesUserScopedBillingUsageEndpoint_ForSignedInAccountGitHubCopilotUsageProviderTestshttps://api.github.com/users/{UserName}/settings/billing/usage(built fromaccount.UserName), not the unscoped/copilot/billing/usageGetMetricsAsync_ParsesUsageItems_IntoCopilotMetricsGitHubCopilotUsageProviderTestsusageItems[]body withproduct":"copilot"items is parsed intoUsageMetricentries (Premium Request / AI Credits)GetMetricsAsync_IgnoresNonCopilotUsageItemsGitHubCopilotUsageProviderTestsactions/modelsline items are filtered out; only Copilot items become metricsGetMetricsAsync_SendsEnhancedBillingHeadersGitHubCopilotUsageProviderTestsAccept: application/vnd.github+jsonandX-GitHub-Api-Version: 2026-03-10, with the Bearer tokenGetMetricsAsync_WhenNotFound_ReturnsEmptyGitHubCopilotUsageProviderTestsGetMetricsAsync_WhenForbidden_LogsPermissionWarningGitHubCopilotUsageProviderTestsPlanread) is logged distinctly from empty/404UsageMetricsService_AccountAdded_WhenProviderReturnsMetricsUsageMetricsServiceTestsgithub.comuser-accountwhose provider returns Copilot metrics is added toUsageMetrics.Accounts(no longer skipped)UsageMetricsService_AccountNotAdded_WhenProviderReturnsEmptyMetricsUsageMetricsServiceTestsMainWindow_UsageTrackerPanel_Visible_WhenAccountWithUsageExistsMainWindowUsageTrackerTestsTopRightLabel != null) once a discovered account yields ≥1 metricTopRightLabel_CopilotAccountAdded_BecomesNonNullUsageTrackerViewModelTestsTopRightLabelbecomes non-null once a Copilot account is added, un-hiding the panelConsidered / Background
user-account→usage relationship in the entity graph", and later as "the toolbar is transitively gated on a provider returning ≥1 metric, and no provider ever does." Both descriptions correctly identified the downstream symptom (empty metrics → account skipped → GUI hidden) but did not pin the upstream cause. The upstream cause is now confirmed: the provider queries the wrong, unscoped endpoint (/copilot/billing/usage) that returns 404. Durable/inspectable usage in the entity graph remains a separate, future concern and should not be conflated with this bug.UsageMetrics.Accountsregardless ofhasMetricsand lettingRecomputeTopRightLabelshow a neutral label — would make the indicator resilient to any single provider failure. Useful defense-in-depth, but with the correct endpoint the provider now returns real metrics, so this is optional.AgentSessionShortcutContext.cs:58-68), (B) host-based provider routing (UsageMetricsService.cs:54-59) — are landed and are not the break. This issue documents the remaining upstream defect: the Copilot provider's request URL is wrong.#1092 Depends on #1086; both areRelated tothis issue. (These were previously tracked as comments and are incorporated here.)Related to #1041.