Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export default function ProviderDetails({
/>
)}
{tab === "usage" && (
<ProviderUsage item={item} usageTotals={usageTotals} quotaReport={quotaReport} modelUsage={modelUsage} />
<ProviderUsage item={item} usageTotals={usageTotals} quotaReport={quotaReport} modelUsage={modelUsage} accounts={accounts} />
)}
{tab === "accounts" && (
<ProviderAuthPanel
Expand Down
43 changes: 40 additions & 3 deletions gui/src/components/provider-workspace/ProviderUsage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,27 @@ import QuotaBars from "../QuotaBars";
import type { WorkspaceItem } from "../../provider-workspace/catalog";
import { formatRelativeTime, relativeTimeLabelsFromT, formatRequestCount, formatTokenCount, formatCostUsd } from "../../provider-workspace/usage";
import { accountQuotaFromReport, formatQuotaSourceLabel, type ProviderQuotaReportView } from "../../provider-workspace/report";
import type { ProviderUsageTotals, ProviderModelUsageRow } from "./types";
import type { ProviderUsageTotals, ProviderModelUsageRow, OAuthAccountRow } from "./types";

export default function ProviderUsage({ item, usageTotals, quotaReport, modelUsage }: {
export default function ProviderUsage({ item, usageTotals, quotaReport, modelUsage, accounts }: {
item: WorkspaceItem;
usageTotals?: ProviderUsageTotals;
quotaReport?: ProviderQuotaReportView;
modelUsage?: ProviderModelUsageRow[];
accounts?: OAuthAccountRow[];
}) {
const t = useT();
const { locale } = useI18n();
const timeLabels = relativeTimeLabelsFromT(t);
const hasUsage = usageTotals?.requests !== undefined;
const quota = accountQuotaFromReport(quotaReport);
const [expandedModel, setExpandedModel] = useState<string | null>(null);
const [selectedAccountId, setSelectedAccountId] = useState<string>("all");

const selectedAccount = useMemo(() => {
if (!accounts?.length || selectedAccountId === "all") return null;
return accounts.find(a => a.id === selectedAccountId) ?? null;
}, [accounts, selectedAccountId]);
Comment on lines 24 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Account selector changes the badge but not the displayed usage numbers.

selectedAccountId and selectedAccount (lines 26-31) are only read to render the badge (lines 78-85). The cost/requests/tokens metrics (lines 88-101) and the model breakdown table use providerCost and sortedModels, both derived solely from the usageTotals/modelUsage props, which stay fixed at the combined provider totals regardless of selectedAccountId.

Concretely: pick an individual account from the dropdown, and the badge shows that account's email, but "Estimated cost", "requests", "tokens", and the "Model breakdown" table below are unchanged — they still show the provider-wide combined figures. This means the selector currently misleads users into thinking they are looking at per-account numbers.

Issue #1063 (linked in the PR objectives) explicitly requires that selecting an account shows "30-day request count, token usage, and estimated cost" for that account, and that "The interface should clearly indicate whether displayed statistics represent overall provider usage or the selected account's usage." As implemented, the interface never shows account-specific statistics — only account-specific identity.

Since OAuthAccountRow (in types.ts) carries no usage/cost fields, the fix needs either:

  1. A new prop such as accountUsageTotals?: Record<string, ProviderUsageTotals> (or similar) passed down from ProviderDetails, keyed by account id, so the metrics block can pick accountUsageTotals[selectedAccountId] ?? usageTotals and sortedModels can filter/aggregate model usage per account, or
  2. If per-account usage isn't available yet from the management API, disable/hide the numeric metrics when a specific account is selected and show an explicit "not yet available per account" message instead of silently showing combined totals under a misleading badge.
🐛 Minimal fix if per-account data is not yet wired
         {hasUsage ? (
           <>
-            <div className="pws-usage-metrics pws-usage-metrics-3" role="group" aria-label={t("pws.usageLast30d")}>
+            <div className="pws-usage-metrics pws-usage-metrics-3" role="group" aria-label={t("pws.usageLast30d")}>
+              {selectedAccount && (
+                <p className="muted pws-cost-disclaimer">{t("pws.accountUsageUnavailable")}</p>
+              )}
               <div className="pws-usage-metric">

Do you want me to draft the per-account data plumbing through ProviderDetails if the management API already exposes per-account usage?

Also applies to: 55-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/components/provider-workspace/ProviderUsage.tsx` around lines 24 -
31, Update ProviderUsage so selecting an individual account does not display
provider-wide usage under an account-specific badge: either wire per-account
usage/model data from ProviderDetails and derive the metrics and sortedModels
from the selected account, falling back to usageTotals only for “all,” or hide
those numeric sections and show an explicit unavailable-per-account message when
data is not provided. Ensure the UI clearly identifies whether statistics
represent overall provider usage or the selected account.

void item;

const sortedModels = useMemo(() => {
Expand All @@ -45,7 +52,37 @@ export default function ProviderUsage({ item, usageTotals, quotaReport, modelUsa
return (
<div className="pws-section">
<div className="pws-usage-block">
<h3 className="pws-section-title">{t("pws.usageLast30d")}</h3>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 12 }}>
<h3 className="pws-section-title" style={{ margin: 0 }}>{t("pws.usageLast30d")}</h3>
{accounts && accounts.length > 0 && (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<label htmlFor="pws-account-filter" className="muted faint" style={{ fontSize: 13 }}>
{t("pws.usageAccountSelector")}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the dashboard guide for the account filter

This introduces a new user-facing workflow under Providers → Usage, but the commit does not update docs-site/; a repository-wide search shows the existing web-dashboard guide still describes provider account and quota management without the new combined-versus-individual usage selector. Document the selector's behavior and scope in the dashboard guide so the shipped UI and user documentation remain synchronized.

AGENTS.md reference: gui/AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

</label>
<select
id="pws-account-filter"
className="select select-sm"
value={selectedAccountId}
onChange={e => setSelectedAccountId(e.target.value)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make the account selector filter the displayed usage

When a multi-account provider user selects an individual account, this handler only changes selectedAccountId, which controls the badge; usageTotals, sortedModels, providerCost, and quota remain derived from the same provider-wide props. Consequently, costs, requests, tokens, model rows, and rate-limit bars stay unchanged and misleadingly appear to belong to the selected account. Pass account-scoped usage and quota data through the management API and derive each display section from the selection, or do not expose individual account options.

AGENTS.md reference: gui/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

>
<option value="all">{t("pws.allAccountsCombined")}</option>
{accounts.map(acc => (
<option key={acc.id} value={acc.id}>
{acc.email ?? acc.alias ?? acc.id} {acc.active ? `(${t("prov.accountActive")})` : ""}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep opaque account IDs out of visible labels

When an OAuth account has neither an email nor an alias, this option renders the complete raw acc.id, and the selected-account badge repeats it. The existing dashboard privacy contract uses oauthAccountDisplayLabel or displayAccountId specifically so opaque storage IDs never become user-visible; this regression exposes the identifier in the UI and in screenshots. Build both labels with the existing safe helper instead of falling back to acc.id.

AGENTS.md reference: gui/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

</option>
))}
</select>
</div>
)}
</div>
{selectedAccount && (
<div style={{ marginTop: 8 }}>
<span className="badge badge-primary">
{selectedAccount.email ?? selectedAccount.alias ?? selectedAccount.id}
{selectedAccount.active ? ` · ${t("prov.accountActive")}` : ""}
</span>
</div>
)}
{hasUsage ? (
<>
<div className="pws-usage-metrics pws-usage-metrics-3" role="group" aria-label={t("pws.usageLast30d")}>
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,12 @@ export const de: Record<TKey, string> = {
"pws.noModelMatch": "Keine Modelle entsprechen dem Filter.",
"pws.adapterBaseRequired": "Adapter und Basis-URL sind erforderlich.",
"pws.addAccount": "Konto hinzufügen",
"pws.importJson": "JSON importieren",
"pws.importJsonCockpit": "JSON importieren (Cockpit)",
"pws.importResultSummary": "{imported} Konto/en importiert, {failed} fehlgeschlagen.",
"pws.usageAccountSelector": "Kontonutzungsfilter",
"pws.allAccountsCombined": "Alle Konten (Kombiniert)",
"prov.expiresAt": "Läuft ab: {date}",
"pws.addKey": "API-Schlüssel hinzufügen",
"pws.apiKeys": "API-Schlüssel",
"pws.authMode": "Auth-Modus",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,12 @@ export const en = {
"pws.noModelMatch": "No models match the filter.",
"pws.adapterBaseRequired": "Adapter and base URL are required.",
"pws.addAccount": "Add account",
"pws.importJson": "Import JSON",
"pws.importJsonCockpit": "Import JSON (Cockpit)",
"pws.importResultSummary": "Imported {imported} account(s), {failed} failed.",
"pws.usageAccountSelector": "Account Usage Filter",
"pws.allAccountsCombined": "All Accounts (Combined)",
"prov.expiresAt": "Expires: {date}",
"pws.addKey": "Add API key",
"pws.apiKeys": "API Keys",
"pws.authMode": "Auth mode",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,12 @@ export const ja: Record<TKey, string> = {
"pws.noModelMatch": "フィルタに一致するモデルがありません。",
"pws.adapterBaseRequired": "アダプターとベース URL は必須です。",
"pws.addAccount": "アカウントを追加",
"pws.importJson": "JSON をインポート",
"pws.importJsonCockpit": "JSON をインポート (Cockpit)",
"pws.importResultSummary": "{imported} 件のアカウントをインポートしました(失敗: {failed} 件)。",
"pws.usageAccountSelector": "アカウント利用フィルター",
"pws.allAccountsCombined": "全アカウント(合計)",
"prov.expiresAt": "有効期限: {date}",
"pws.addKey": "API キーを追加",
"pws.apiKeys": "API キー",
"pws.authMode": "認証モード",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,12 @@ export const ko: Record<TKey, string> = {
"pws.noModelMatch": "필터와 일치하는 모델이 없습니다.",
"pws.adapterBaseRequired": "어댑터와 기본 URL은 필수입니다.",
"pws.addAccount": "계정 추가",
"pws.importJson": "JSON 가져오기",
"pws.importJsonCockpit": "JSON 가져오기 (Cockpit)",
"pws.importResultSummary": "{imported}개 계정을 가져왔습니다 ({failed}개 실패).",
"pws.usageAccountSelector": "계정 사용 필터",
"pws.allAccountsCombined": "모든 계정 (합계)",
"prov.expiresAt": "만료일: {date}",
"pws.addKey": "API 키 추가",
"pws.apiKeys": "API 키",
"pws.authMode": "인증 방식",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,12 @@ export const ru: Record<TKey, string> = {
"pws.noModelMatch": "Нет моделей, соответствующих фильтру.",
"pws.adapterBaseRequired": "Укажите адаптер и базовый URL.",
"pws.addAccount": "Добавить аккаунт",
"pws.importJson": "Импортировать JSON",
"pws.importJsonCockpit": "Импорт JSON (Cockpit)",
"pws.importResultSummary": "Импортировано аккаунтов: {imported}, ошибок: {failed}.",
"pws.usageAccountSelector": "Фильтр использования по аккаунту",
"pws.allAccountsCombined": "Все аккаунты (суммарно)",
"prov.expiresAt": "Истекает: {date}",
"pws.addKey": "Добавить API-ключ",
"pws.apiKeys": "API-ключи",
"pws.authMode": "Режим аутентификации",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,12 @@ export const zh: Record<TKey, string> = {
"pws.noModelMatch": "没有匹配筛选的模型。",
"pws.adapterBaseRequired": "适配器和基本 URL 为必填项。",
"pws.addAccount": "添加账户",
"pws.importJson": "导入 JSON",
"pws.importJsonCockpit": "导入 JSON (Cockpit)",
"pws.importResultSummary": "已导入 {imported} 个账户,{failed} 个失败。",
"pws.usageAccountSelector": "账户使用筛选",
"pws.allAccountsCombined": "所有账户(汇总)",
"prov.expiresAt": "到期时间: {date}",
"pws.addKey": "添加 API 密钥",
"pws.apiKeys": "API 密钥",
"pws.authMode": "认证方式",
Expand Down
Loading