diff --git a/domain/provider-presentation.ts b/domain/provider-presentation.ts
index 8d50a35..cb6d665 100644
--- a/domain/provider-presentation.ts
+++ b/domain/provider-presentation.ts
@@ -5,6 +5,8 @@ export interface ProviderPresentation {
readonly darkMarkPath?: string;
readonly apiKeySetupUrl?: string;
readonly apiKeyGuide?: "elevenlabs" | "newapi";
+ /** Provider usage page with a stable public URL. Absent when none is known. */
+ readonly usagePageUrl?: string;
}
export const providerCatalog = {
@@ -12,12 +14,14 @@ export const providerCatalog = {
displayName: "ChatGPT",
presentation: {
markPath: "/provider-marks/chatgpt.svg",
+ usagePageUrl: "https://chatgpt.com/codex/settings/usage",
},
},
claude: {
displayName: "Claude",
presentation: {
markPath: "/provider-marks/claude.svg",
+ usagePageUrl: "https://claude.ai/settings/usage",
},
},
kimi: {
@@ -25,6 +29,7 @@ export const providerCatalog = {
presentation: {
markPath: "/provider-marks/kimi.svg",
darkMarkPath: "/provider-marks/kimi-dark.svg",
+ usagePageUrl: "https://www.kimi.com/settings/subscription",
},
},
cursor: {
@@ -32,6 +37,7 @@ export const providerCatalog = {
presentation: {
markPath: "/provider-marks/cursor.svg",
darkMarkPath: "/provider-marks/cursor-dark.svg",
+ usagePageUrl: "https://cursor.com/dashboard/spending",
},
},
grok: {
@@ -45,12 +51,14 @@ export const providerCatalog = {
displayName: "Mistral",
presentation: {
markPath: "/provider-marks/mistral.svg",
+ usagePageUrl: "https://admin.mistral.ai/organization/usage",
},
},
perplexity: {
displayName: "Perplexity",
presentation: {
markPath: "/provider-marks/perplexity.svg",
+ usagePageUrl: "https://www.perplexity.ai/#settings/usage",
},
},
elevenlabs: {
@@ -125,6 +133,7 @@ export const providerCatalog = {
presentation: {
markPath: "/provider-marks/openai.svg",
apiKeySetupUrl: "https://platform.openai.com/api-keys",
+ usagePageUrl: "https://platform.openai.com/usage",
},
},
groqcloud: {
diff --git a/entrypoints/sidepanel/Cockpit.test.tsx b/entrypoints/sidepanel/Cockpit.test.tsx
index e83c370..e4e876c 100644
--- a/entrypoints/sidepanel/Cockpit.test.tsx
+++ b/entrypoints/sidepanel/Cockpit.test.tsx
@@ -1474,6 +1474,66 @@ describe("Cockpit", () => {
expect(screen.getByRole("button", { name: "Disconnect ChatGPT" })).toHaveFocus();
});
+ it("links provider detail to the plan usage page when that page has a stable URL", () => {
+ const state = createFixtureState(NOW);
+ const kimi = state.instances.find((instance) => instance.providerKind === "kimi");
+ if (kimi) {
+ state.instances.push({
+ ...kimi,
+ id: "perplexity:default",
+ providerKind: "perplexity",
+ snapshot: kimi.snapshot
+ ? { ...kimi.snapshot, providerKind: "perplexity" }
+ : undefined,
+ });
+ }
+
+ render(
+ ,
+ );
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "Open ChatGPT details" }),
+ );
+ const usagePage = screen.getByRole("link", {
+ name: "Open the ChatGPT usage page",
+ });
+ expect(usagePage).toHaveAttribute(
+ "href",
+ "https://chatgpt.com/codex/settings/usage",
+ );
+ expect(usagePage).toHaveAttribute("target", "_blank");
+ expect(usagePage).toHaveAttribute("rel", "noopener noreferrer");
+ expect(usagePage).toHaveTextContent("Usage page");
+
+ fireEvent.click(screen.getByRole("button", { name: "Overview" }));
+ fireEvent.click(screen.getByRole("button", { name: "Open Kimi details" }));
+ expect(
+ screen.getByRole("link", { name: "Open the Kimi usage page" }),
+ ).toHaveAttribute("href", "https://www.kimi.com/settings/subscription");
+
+ fireEvent.click(screen.getByRole("button", { name: "Overview" }));
+ fireEvent.click(
+ screen.getByRole("button", { name: "Open Perplexity details" }),
+ );
+ expect(
+ screen.getByRole("link", { name: "Open the Perplexity usage page" }),
+ ).toHaveAttribute("href", "https://www.perplexity.ai/#settings/usage");
+
+ fireEvent.click(screen.getByRole("button", { name: "Overview" }));
+ fireEvent.click(screen.getByRole("button", { name: "Open Grok details" }));
+ expect(
+ screen.queryByRole("link", { name: /usage page/i }),
+ ).not.toBeInTheDocument();
+ });
+
it("returns to Overview from Settings after disconnecting the provider that opened it", () => {
const onDisconnectInstance = vi.fn();
render(
diff --git a/entrypoints/sidepanel/Cockpit.tsx b/entrypoints/sidepanel/Cockpit.tsx
index 5179fc3..af208e9 100644
--- a/entrypoints/sidepanel/Cockpit.tsx
+++ b/entrypoints/sidepanel/Cockpit.tsx
@@ -389,6 +389,7 @@ export function providerView(
: undefined,
detailTables: snapshot?.detailTables,
now,
+ usagePageUrl: providerPresentation(provider.providerKind).usagePageUrl,
};
}
diff --git a/entrypoints/sidepanel/components/Icon.tsx b/entrypoints/sidepanel/components/Icon.tsx
index 7bd62e5..868615d 100644
--- a/entrypoints/sidepanel/components/Icon.tsx
+++ b/entrypoints/sidepanel/components/Icon.tsx
@@ -14,7 +14,8 @@ export type IconName =
| "settings"
| "trash"
| "trending-down"
- | "trending-up";
+ | "trending-up"
+ | "external";
export interface IconProps {
name: IconName;
@@ -83,6 +84,12 @@ export function Icon({ name, className = "" }: IconProps) {
>
+ ) : name === "external" ? (
+ <>
+
+
+
+ >
) : name === "code" ? (
) : (
diff --git a/entrypoints/sidepanel/components/ProviderCard.tsx b/entrypoints/sidepanel/components/ProviderCard.tsx
index d99f870..f0d8344 100644
--- a/entrypoints/sidepanel/components/ProviderCard.tsx
+++ b/entrypoints/sidepanel/components/ProviderCard.tsx
@@ -81,6 +81,7 @@ export interface ProviderCardProps {
onClick: () => void;
};
headingLevel?: 2 | 3;
+ usagePageUrl?: string;
openDetailsFocusKey?: string;
onOpenDetails?: () => void;
onOpenHistory?: (metricId: string) => void;
diff --git a/entrypoints/sidepanel/styles.css b/entrypoints/sidepanel/styles.css
index 1aa0cd7..ac7daf3 100644
--- a/entrypoints/sidepanel/styles.css
+++ b/entrypoints/sidepanel/styles.css
@@ -1951,17 +1951,30 @@ section[aria-labelledby="overview-title"] .provider-list {
overflow-wrap: anywhere;
}
+.detail-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
.detail-history-action {
display: inline-flex;
min-height: 44px;
align-items: center;
justify-content: center;
gap: 6px;
+ padding: 0 12px;
border: 1px solid var(--border);
border-radius: 11px;
color: var(--text);
font-size: 12px;
font-weight: 600;
+ text-decoration: none;
+}
+
+a.detail-history-action:hover {
+ color: var(--text);
+ border-color: var(--quota);
}
.connection-surface__disconnect,
diff --git a/entrypoints/sidepanel/views/ProviderDetailView.tsx b/entrypoints/sidepanel/views/ProviderDetailView.tsx
index 5fa9990..13ec2ec 100644
--- a/entrypoints/sidepanel/views/ProviderDetailView.tsx
+++ b/entrypoints/sidepanel/views/ProviderDetailView.tsx
@@ -265,19 +265,37 @@ export function ProviderDetailView({
/>
) : null}
- {firstWindow ? (
-
+ {firstWindow || provider.usagePageUrl ? (
+
+ {firstWindow ? (
+
+ ) : null}
+ {provider.usagePageUrl ? (
+
+
+ {l10n.t("providerDetail.openUsage")}
+
+ ) : null}
+
) : null}
diff --git a/locales/en.yml b/locales/en.yml
index d7a4326..d36e3f8 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -122,6 +122,8 @@ providerDetail:
usageGroup: Usage group
openHistory: Open history
openHistoryNamed: "Open {label} history"
+ openUsage: Usage page
+ openUsageNamed: "Open the {label} usage page"
connectionTitle: Connection and capabilities
scheduledNoTab: Scheduled refresh reads the existing session without opening a tab.
disconnectExplanation: Disconnecting permanently deletes this provider's credentials, configuration, usage, and history.
diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml
index fb52089..375afe8 100644
--- a/locales/zh_CN.yml
+++ b/locales/zh_CN.yml
@@ -122,6 +122,8 @@ providerDetail:
usageGroup: 用量组
openHistory: 打开历史
openHistoryNamed: "打开 {label} 的历史"
+ openUsage: 用量页面
+ openUsageNamed: "打开 {label} 的用量页面"
connectionTitle: 连接与能力
scheduledNoTab: 定时刷新会读取现有会话,不会打开标签页。
disconnectExplanation: 断开连接会永久删除该服务的凭据、配置、用量和历史。
diff --git a/providers/catalog.test.ts b/providers/catalog.test.ts
index 1e0ce24..2453741 100644
--- a/providers/catalog.test.ts
+++ b/providers/catalog.test.ts
@@ -51,6 +51,7 @@ describe("provider catalog", () => {
darkMarkPath?: string;
apiKeySetupUrl?: string;
apiKeyGuide?: "elevenlabs" | "newapi";
+ usagePageUrl?: string;
};
const providerPresentation = (
catalog as typeof catalog & {
@@ -70,20 +71,24 @@ describe("provider catalog", () => {
{
providerId: "chatgpt",
markPath: "/provider-marks/chatgpt.svg",
+ usagePageUrl: "https://chatgpt.com/codex/settings/usage",
},
{
providerId: "claude",
markPath: "/provider-marks/claude.svg",
+ usagePageUrl: "https://claude.ai/settings/usage",
},
{
providerId: "kimi",
markPath: "/provider-marks/kimi.svg",
darkMarkPath: "/provider-marks/kimi-dark.svg",
+ usagePageUrl: "https://www.kimi.com/settings/subscription",
},
{
providerId: "cursor",
markPath: "/provider-marks/cursor.svg",
darkMarkPath: "/provider-marks/cursor-dark.svg",
+ usagePageUrl: "https://cursor.com/dashboard/spending",
},
{
providerId: "grok",
@@ -93,10 +98,12 @@ describe("provider catalog", () => {
{
providerId: "mistral",
markPath: "/provider-marks/mistral.svg",
+ usagePageUrl: "https://admin.mistral.ai/organization/usage",
},
{
providerId: "perplexity",
markPath: "/provider-marks/perplexity.svg",
+ usagePageUrl: "https://www.perplexity.ai/#settings/usage",
},
{
providerId: "elevenlabs",
@@ -149,6 +156,7 @@ describe("provider catalog", () => {
providerId: "openai",
markPath: "/provider-marks/openai.svg",
apiKeySetupUrl: "https://platform.openai.com/api-keys",
+ usagePageUrl: "https://platform.openai.com/usage",
},
{
providerId: "groqcloud",