Files
paseo/packages/server/src/services/quota-fetcher/providers/grok.ts
T
acea7f3d24 feat: live multi-provider quota panel (#1278)
* feat: live provider quota panel (Claude + Codex)

Adds Claude and Codex plan usage to the context window percentage circle tooltip, querying their respective APIs directly using existing CLI auth tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address review feedback - restore quota trigger, guard NaN date

- Re-add triggerFetch() on agent idle/error in agent status subscription
- Guard formatResetsAtLabel against NaN from malformed API date strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve composer conflict and stub subscribe in websocket tests

* feat(quota): add cursor, copilot, zai, grok, and kimi quota UI and improve type safety

* feat(quota): make Claude and Codex pluggable, and map additional quota/credits metrics

* security(quota): fix shell injection vulnerability by migrating exec to execFile

* revert: restore original scripts/dev.ps1 and packages/desktop/scripts/dev.ps1

* Fix i18n resource test expectation

* Fix quota tooltip empty-provider state

* Preserve zero values in Grok quota

* Fix empty quota fetch state

* Ignore quota frames in relay reconnect tests

* Persist refreshed Codex tokens to source auth file

* fix(quota): read Claude OAuth credentials from the macOS login Keychain

On macOS, Claude Code stores its OAuth credential in the login Keychain
(generic-password item, service "Claude Code-credentials"), not in
~/.claude/.credentials.json — that file usually does not exist there, so the
Claude provider's existsSync check failed and macOS users silently got no
Claude quota. Read it via the macOS `security` CLI (its ACL only trusts
/usr/bin/security to decrypt; a native Keychain read would prompt), and stay
read-only there since Claude Code owns the refresh/persist. Linux and Windows
keep using ~/.claude/.credentials.json unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix quota keychain timeout and local host reconciliation

* Fix terminal notification test quota stub

* Ignore quota frames in terminal notification tests

* fix(quota): bypass Claude token refresh on macOS Keychain-backed logins

* fix(dev): update local dev daemon port to 6768 on Windows scripts/dev.ps1

* fix(dev): bypass Unix dev-daemon.sh on Windows in dev.ps1

* fix(client): handle and dispatch provider_quota event in DaemonClient

* fix(desktop): fix PowerShell quote stripping in desktop dev.ps1 config seeding

* fix(desktop): execute config update via temp file to avoid PowerShell quoting bugs

* fix(desktop): fix concurrently command invocation on Windows in dev.ps1

* fix(desktop): resolve path escaping and environment setting syntax errors in dev.ps1 for Windows

* Add on-demand provider usage views

* Fix Cursor usage billing dates

* Move provider usage renderer coverage to e2e

* Use provider manifest for quota fetchers

* Add provider usage fetch timeouts

* Reshape provider usage fetchers

---------

Co-authored-by: ABorakati <ABorakati@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: lumingjun <lumingjun@bytedance.com>
2026-06-20 12:19:50 +08:00

119 lines
3.1 KiB
TypeScript

import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderUsage, ProviderUsageBalance } from "../../../server/messages.js";
import type { ProviderApiFetch, ProviderUsageFetcher } from "../provider.js";
import {
ApiNumberSchema,
balanceToneFromRemaining,
fetchProviderApi,
unavailableUsage,
} from "../usage.js";
const GrokUsageResponseSchema = z.object({
config: z
.object({
monthlyLimit: z
.object({
val: ApiNumberSchema.optional(),
})
.nullish(),
})
.nullish(),
usage: z
.object({
creditUsage: ApiNumberSchema.optional(),
})
.nullish(),
});
const GrokAuthSchema = z.object({
access_token: z.string().optional(),
});
interface GrokQuotaProviderOptions {
logger: Logger;
fetch?: ProviderApiFetch;
}
export class GrokQuotaProvider implements ProviderUsageFetcher {
readonly providerId = "grok";
readonly displayName = "Grok";
private readonly logger: Logger;
private readonly fetchApi: ProviderApiFetch;
constructor(options: GrokQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<ProviderUsage> {
const token =
process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"] || (await this.readGrokToken());
if (!token) return unavailableUsage(this);
const res = await fetchProviderApi(
this.fetchApi,
"https://cli-chat-proxy.grok.com/v1/billing",
{
headers: {
Authorization: `Bearer ${token}`,
"X-XAI-Token-Auth": "xai-grok-cli",
Accept: "application/json",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Grok usage fetch failed");
return unavailableUsage(this);
}
const resp = GrokUsageResponseSchema.parse(await res.json());
const monthlyLimit = resp.config?.monthlyLimit?.val ?? null;
const creditUsage = resp.usage?.creditUsage ?? null;
const balances: ProviderUsageBalance[] = [];
if (monthlyLimit !== null || creditUsage !== null) {
const remaining =
monthlyLimit !== null && creditUsage !== null
? Math.max(0, monthlyLimit - creditUsage)
: null;
balances.push({
id: "monthly_credits",
label: "Monthly credits",
used: creditUsage,
remaining,
limit: monthlyLimit,
unit: "credits",
tone: balanceToneFromRemaining(remaining),
});
}
return {
providerId: this.providerId,
displayName: this.displayName,
status: "available",
planLabel: null,
windows: [],
balances,
details: [],
error: null,
};
}
private async readGrokToken(): Promise<string | null> {
const path = join(homedir(), ".grok", "auth.json");
if (!existsSync(path)) return null;
try {
const auth = GrokAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8")));
return auth.access_token ?? null;
} catch {
return null;
}
}
}