@diousk/pi-usage-fast 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,116 @@
1
+ export {
2
+ CODEX_FAST_MODEL_IDS,
3
+ CODEX_FAST_SERVICE_TIER,
4
+ CODEX_STANDARD_SERVICE_TIER,
5
+ codexFastAvailability,
6
+ codexFastIsEffective,
7
+ codexFastRequestTier,
8
+ codexFastStatusLabel,
9
+ correctCodexFastMessageCost,
10
+ rewriteCodexFastPayload,
11
+ } from "./codex-fast.js";
12
+ export type {
13
+ CodexResetAvailability,
14
+ CodexResetOption,
15
+ CodexResetOutcome,
16
+ CodexResetOutcomeCode,
17
+ } from "./codex-resets.js";
18
+ export {
19
+ consumeCodexResetCredit,
20
+ listCodexResetCredits,
21
+ normalizeCodexResetCreditsPayload,
22
+ resolveCodexResetAuth,
23
+ } from "./codex-resets.js";
24
+ export {
25
+ abortError,
26
+ awaitWithDeadline,
27
+ errorMessage,
28
+ fingerprintResolvedAuth,
29
+ redactUsageError,
30
+ runWithConcurrency,
31
+ sanitizeDisplayText,
32
+ UsageCache,
33
+ } from "./core.js";
34
+ export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
35
+ export { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
36
+ export { normalizeCodexBackendPayload } from "./providers/codex.js";
37
+ export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
38
+ export {
39
+ createFireworksAdapter,
40
+ normalizeFireworksAccountsPayload,
41
+ normalizeFireworksBillingSummaryPayload,
42
+ } from "./providers/fireworks.js";
43
+ export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
44
+ export { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
45
+ export type { MiniMaxProviderId, MiniMaxUsageKind } from "./providers/minimax.js";
46
+ export {
47
+ miniMaxUsageKind,
48
+ normalizeMiniMaxUsagePayload,
49
+ } from "./providers/minimax.js";
50
+ export type { MoonshotProviderId } from "./providers/moonshot.js";
51
+ export { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
52
+ export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
53
+ export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
54
+ export { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
55
+ export { normalizeXaiBillingPayload } from "./providers/xai.js";
56
+ export { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
57
+ export {
58
+ adapterForProvider,
59
+ isStaleExtensionContextError,
60
+ providerIsConfigured,
61
+ queryProviderUsage,
62
+ resolveUsageAuth,
63
+ SUPPORTED_ADAPTERS,
64
+ usageAdapters,
65
+ XAI_ADAPTER,
66
+ } from "./query.js";
67
+ export type {
68
+ CodexStatusPercentage,
69
+ UsageSettings,
70
+ UsageSettingsRuntime,
71
+ UsageSettingsState,
72
+ UsageTargetPublicationCheck,
73
+ } from "./settings.js";
74
+ export {
75
+ createUsageSettingsRuntime,
76
+ DEFAULT_USAGE_SETTINGS,
77
+ loadUsageSettings,
78
+ normalizeUsageSettings,
79
+ usageSettingsPath,
80
+ } from "./settings.js";
81
+ export type {
82
+ BasetenBillingUsagePayload,
83
+ DeepSeekBalancePayload,
84
+ FireworksAccountsPayload,
85
+ FireworksBillingSummaryPayload,
86
+ KimiCodingUsagePayload,
87
+ MiniMaxUsagePayload,
88
+ MoonshotBalancePayload,
89
+ ProviderUsageState,
90
+ ResolvedUsageAuth,
91
+ UsageBucket,
92
+ UsageDisplayState,
93
+ UsageMetric,
94
+ UsageModel,
95
+ UsageProviderAdapter,
96
+ UsageProviderTarget,
97
+ UsageQuerySettings,
98
+ UsageReport,
99
+ UsageRequestGuard,
100
+ UsageSemantics,
101
+ UsageSemanticsKind,
102
+ UsageTargetResolver,
103
+ UsageUnit,
104
+ VercelAIGatewayCreditsPayload,
105
+ XaiBillingPayload,
106
+ XaiUserPayload,
107
+ } from "./types.js";
108
+ export { default } from "./usage.js";
109
+ export type { UsageTargetResolution, UsageTargetSelectOptions } from "./usage-targets.js";
110
+ export {
111
+ createUsageTargetSelectOptions,
112
+ isBoundedTargetId,
113
+ listUsageTargets,
114
+ normalizeUsageTargets,
115
+ resolveUsageTarget,
116
+ } from "./usage-targets.js";
@@ -0,0 +1,117 @@
1
+ import type { OAuthCredential } from "@earendil-works/pi-ai";
2
+ import { type ExtensionAPI, type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
3
+
4
+ export const OAUTH_CREDENTIAL_SOURCE_CHANNEL = "oauth:credential-source:v1";
5
+ export const OAUTH_CREDENTIAL_READINESS_CHANNEL = "oauth:credential-readiness:v1";
6
+
7
+ export type StoredCredentialReader = (providerId: string) => unknown;
8
+
9
+ export type OAuthCredentialCandidates =
10
+ | { ok: true; candidates: readonly OAuthCredential[]; offeredCount?: number }
11
+ | { ok: false };
12
+
13
+ export type OAuthCredentialCandidateReader = {
14
+ (ctx: ExtensionContext, providerId: string): OAuthCredentialCandidates | Promise<OAuthCredentialCandidates>;
15
+ waitUntilReady?(ctx: ExtensionContext, providerId: string): Promise<boolean>;
16
+ };
17
+
18
+ export function createOAuthCredentialCandidateReader(
19
+ pi: ExtensionAPI,
20
+ credentialReader: StoredCredentialReader = readStoredCredential,
21
+ ): OAuthCredentialCandidateReader {
22
+ const reader: OAuthCredentialCandidateReader = (ctx, providerId) =>
23
+ collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
24
+ reader.waitUntilReady = (ctx, providerId) => waitForOAuthCredentialSources(pi, ctx, providerId);
25
+ return reader;
26
+ }
27
+
28
+ export function collectOAuthCredentialCandidates(
29
+ pi: Pick<ExtensionAPI, "events">,
30
+ ctx: ExtensionContext,
31
+ providerId: string,
32
+ credentialReader: StoredCredentialReader = readStoredCredential,
33
+ ): OAuthCredentialCandidates {
34
+ const candidates: OAuthCredential[] = [];
35
+ let collecting = true;
36
+ const request = Object.freeze({
37
+ session: ctx.sessionManager,
38
+ provider: providerId,
39
+ offer(candidate: unknown) {
40
+ if (!collecting) return;
41
+ const clone = cloneOAuthCredential(candidate);
42
+ if (clone) candidates.push(clone);
43
+ },
44
+ });
45
+ try {
46
+ pi.events.emit(OAUTH_CREDENTIAL_SOURCE_CHANNEL, request);
47
+ } catch {
48
+ return { ok: false };
49
+ } finally {
50
+ collecting = false;
51
+ }
52
+ const offeredCount = candidates.length;
53
+ try {
54
+ const fallback = cloneOAuthCredential(credentialReader(providerId));
55
+ if (fallback) candidates.push(fallback);
56
+ } catch {
57
+ // A malformed or unavailable standalone credential is equivalent to no fallback.
58
+ }
59
+ return { ok: true, candidates, offeredCount };
60
+ }
61
+
62
+ async function waitForOAuthCredentialSources(
63
+ pi: Pick<ExtensionAPI, "events">,
64
+ ctx: ExtensionContext,
65
+ providerId: string,
66
+ ): Promise<boolean> {
67
+ const pending: Promise<unknown>[] = [];
68
+ let collecting = true;
69
+ const request = Object.freeze({
70
+ session: ctx.sessionManager,
71
+ provider: providerId,
72
+ waitUntil(value: unknown) {
73
+ if (!collecting || !(value instanceof Promise)) return;
74
+ pending.push(value);
75
+ },
76
+ });
77
+ try {
78
+ pi.events.emit(OAUTH_CREDENTIAL_READINESS_CHANNEL, request);
79
+ } catch {
80
+ return false;
81
+ } finally {
82
+ collecting = false;
83
+ }
84
+ try {
85
+ await Promise.all(pending);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ export function fallbackOAuthCredentialCandidates(
93
+ providerId: string,
94
+ credentialReader: StoredCredentialReader,
95
+ ): OAuthCredentialCandidates {
96
+ try {
97
+ const credential = cloneOAuthCredential(credentialReader(providerId));
98
+ return { ok: true, candidates: credential ? [credential] : [], offeredCount: 0 };
99
+ } catch {
100
+ return { ok: true, candidates: [], offeredCount: 0 };
101
+ }
102
+ }
103
+
104
+ function cloneOAuthCredential(value: unknown): OAuthCredential | undefined {
105
+ try {
106
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
107
+ const clone = structuredClone(value) as OAuthCredential;
108
+ if (!clone || typeof clone !== "object" || Array.isArray(clone)) return undefined;
109
+ if (clone.type !== "oauth") return undefined;
110
+ if (typeof clone.access !== "string" || !clone.access) return undefined;
111
+ if (typeof clone.refresh !== "string" || !clone.refresh) return undefined;
112
+ if (typeof clone.expires !== "number" || !Number.isFinite(clone.expires)) return undefined;
113
+ return clone;
114
+ } catch {
115
+ return undefined;
116
+ }
117
+ }
@@ -0,0 +1,51 @@
1
+ import type { BasetenBillingUsagePayload, UsageMetric, UsageReport } from "../types.js";
2
+
3
+ const DECIMAL_AMOUNT = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
4
+
5
+ export function normalizeBasetenBillingUsagePayload(
6
+ payload: BasetenBillingUsagePayload,
7
+ capturedAt: number,
8
+ ): UsageReport {
9
+ if (payload.model_apis_usage === undefined || payload.model_apis_usage === null) {
10
+ return report(capturedAt, [], ["Baseten returned no Model APIs usage for the last 30 days."]);
11
+ }
12
+ const usage = asObject(payload.model_apis_usage);
13
+ if (!usage) throw new Error("Baseten Model APIs usage was not an object.");
14
+ const metrics: UsageMetric[] = [
15
+ metric("gross-usage", "Gross usage", usage.total),
16
+ metric("credits-used", "Credits used", usage.credits_used),
17
+ metric("net-subtotal", "Net subtotal", usage.subtotal),
18
+ ];
19
+ return report(capturedAt, metrics);
20
+ }
21
+
22
+ function report(capturedAt: number, metrics: UsageMetric[], notes?: string[]): UsageReport {
23
+ return {
24
+ providerId: "baseten",
25
+ providerName: "Baseten",
26
+ capturedAt,
27
+ source: "baseten-billing-usage-summary",
28
+ semantics: { kind: "api-key", label: "Organization Model APIs spend" },
29
+ buckets: [],
30
+ metrics,
31
+ ...(notes ? { notes } : {}),
32
+ };
33
+ }
34
+
35
+ function metric(id: string, label: string, value: unknown): UsageMetric {
36
+ const amount = decimalAmount(value, label);
37
+ return { id, label, value: amount, unit: "currency", currency: "USD" };
38
+ }
39
+
40
+ function decimalAmount(value: unknown, label: string): string {
41
+ const normalized = typeof value === "number" && Number.isFinite(value) ? String(value) : value;
42
+ if (typeof normalized !== "string" || normalized.length > 64 || !DECIMAL_AMOUNT.test(normalized)) {
43
+ throw new Error(`Baseten ${label.toLowerCase()} was not a valid nonnegative amount.`);
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ function asObject(value: unknown): Record<string, unknown> | undefined {
49
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
50
+ return value as Record<string, unknown>;
51
+ }
@@ -0,0 +1,139 @@
1
+ import { sanitizeDisplayText } from "../core.js";
2
+ import type { CodexBackendPayload, UsageBucket, UsageMetric, UsageReport } from "../types.js";
3
+
4
+ export function normalizeCodexBackendPayload(payload: CodexBackendPayload, capturedAt: number): UsageReport {
5
+ const buckets: UsageBucket[] = [];
6
+ normalizeRateLimitGroup(buckets, "codex", "Codex", payload.rate_limit, false);
7
+
8
+ const additional = Array.isArray(payload.additional_rate_limits) ? payload.additional_rate_limits : [];
9
+ for (const item of additional) {
10
+ const value = asObject(item);
11
+ const id = asString(value?.metered_feature) ?? asString(value?.limit_name);
12
+ if (!value || !id) continue;
13
+ try {
14
+ normalizeRateLimitGroup(buckets, id, asString(value.limit_name) ?? id, value.rate_limit, true);
15
+ } catch {
16
+ // Optional provider-specific buckets must not hide otherwise useful primary data.
17
+ }
18
+ }
19
+
20
+ const metrics: UsageMetric[] = [];
21
+ const credits = asObject(payload.credits);
22
+ if (credits?.has_credits === true) {
23
+ if (credits.unlimited === true) {
24
+ metrics.push({ id: "credits", label: "Credits", value: "unlimited" });
25
+ } else {
26
+ const balance = asNumber(credits.balance);
27
+ if (balance !== undefined) {
28
+ metrics.push({ id: "credits", label: "Credits", value: balance, unit: "count" });
29
+ } else {
30
+ metrics.push({ id: "credits", label: "Credits", value: "available" });
31
+ }
32
+ }
33
+ } else if (credits?.has_credits === false) {
34
+ metrics.push({ id: "credits", label: "Credits", value: "none" });
35
+ }
36
+ const resetCredits = asObject(payload.rate_limit_reset_credits);
37
+ const resetCount = asNonnegativeInteger(resetCredits?.available_count);
38
+ if (resetCount !== undefined) {
39
+ metrics.push({
40
+ id: "reset-credits",
41
+ label: "Usage limit resets",
42
+ value: resetCount,
43
+ unit: "count",
44
+ });
45
+ }
46
+ if (buckets.length === 0 && metrics.length === 0) {
47
+ throw new Error("Codex usage endpoint returned no displayable usage data.");
48
+ }
49
+
50
+ const planType = asString(payload.plan_type);
51
+ return {
52
+ providerId: "openai-codex",
53
+ providerName: "OpenAI Codex",
54
+ capturedAt,
55
+ source: "codex-pi-auth",
56
+ semantics: {
57
+ kind: "consumer-subscription",
58
+ label: "ChatGPT subscription limits",
59
+ },
60
+ buckets,
61
+ metrics,
62
+ ...(planType ? { notes: [`Plan: ${planType}`] } : {}),
63
+ };
64
+ }
65
+
66
+ function normalizeRateLimitGroup(
67
+ buckets: UsageBucket[],
68
+ groupId: string,
69
+ groupLabel: string,
70
+ raw: unknown,
71
+ optional: boolean,
72
+ ): void {
73
+ if (raw === undefined || raw === null) return;
74
+ const details = asObject(raw);
75
+ if (!details) {
76
+ if (optional) return;
77
+ throw new Error("Codex rate limit was not an object.");
78
+ }
79
+ addWindow(buckets, groupId, groupLabel, "primary", details.primary_window);
80
+ addWindow(buckets, groupId, groupLabel, "secondary", details.secondary_window);
81
+ }
82
+
83
+ function addWindow(
84
+ buckets: UsageBucket[],
85
+ groupId: string,
86
+ groupLabel: string,
87
+ position: "primary" | "secondary",
88
+ raw: unknown,
89
+ ): void {
90
+ if (raw === undefined || raw === null) return;
91
+ const value = asObject(raw);
92
+ if (!value) throw new Error("Codex rate-limit window was not an object.");
93
+ const used = asNumber(value.used_percent);
94
+ if (used === undefined) return;
95
+ const seconds = asNumber(value.limit_window_seconds);
96
+ const resetsAt = asNumber(value.reset_at);
97
+ buckets.push({
98
+ id: `${groupId}:${position}`,
99
+ label: position === "primary" ? "Primary limit" : "Secondary limit",
100
+ groupId,
101
+ groupLabel,
102
+ modelKeys: [groupId, groupLabel],
103
+ used,
104
+ remaining: 100 - clampPercent(used),
105
+ limit: 100,
106
+ unit: "percent",
107
+ ...(seconds !== undefined && seconds > 0 ? { windowMinutes: Math.ceil(seconds / 60) } : {}),
108
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
109
+ });
110
+ }
111
+
112
+ function asObject(value: unknown): Record<string, unknown> | undefined {
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
114
+ return value as Record<string, unknown>;
115
+ }
116
+
117
+ function asString(value: unknown): string | undefined {
118
+ if (typeof value !== "string") return undefined;
119
+ return sanitizeDisplayText(value, 160) || undefined;
120
+ }
121
+
122
+ function asNumber(value: unknown): number | undefined {
123
+ if (typeof value === "number" && Number.isFinite(value)) return value;
124
+ if (typeof value === "string" && value.trim()) {
125
+ const parsed = Number(value);
126
+ return Number.isFinite(parsed) ? parsed : undefined;
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ function asNonnegativeInteger(value: unknown): number | undefined {
132
+ const parsed = asNumber(value);
133
+ if (parsed === undefined || !Number.isSafeInteger(parsed)) return undefined;
134
+ return Math.max(0, parsed);
135
+ }
136
+
137
+ function clampPercent(value: number): number {
138
+ return Math.min(100, Math.max(0, value));
139
+ }
@@ -0,0 +1,80 @@
1
+ import type { DeepSeekBalancePayload, UsageMetric, UsageReport } from "../types.js";
2
+
3
+ const CURRENCIES = ["CNY", "USD"] as const;
4
+ type DeepSeekCurrency = (typeof CURRENCIES)[number];
5
+
6
+ const BALANCE_FIELDS = [
7
+ ["total", "Total balance", "total_balance"],
8
+ ["granted", "Granted balance", "granted_balance"],
9
+ ["topped-up", "Topped-up balance", "topped_up_balance"],
10
+ ] as const;
11
+
12
+ export function normalizeDeepSeekBalancePayload(payload: DeepSeekBalancePayload, capturedAt: number): UsageReport {
13
+ if (typeof payload.is_available !== "boolean") {
14
+ throw new Error("DeepSeek API balance response availability was not a boolean.");
15
+ }
16
+ if (!Array.isArray(payload.balance_infos) || payload.balance_infos.length === 0) {
17
+ throw new Error("DeepSeek API balance response returned no balance information.");
18
+ }
19
+
20
+ const balances = new Map<DeepSeekCurrency, Record<string, unknown>>();
21
+ for (const raw of payload.balance_infos) {
22
+ const balance = asObject(raw);
23
+ if (!balance) throw new Error("DeepSeek API balance row was not an object.");
24
+ const currency = deepSeekCurrency(balance.currency);
25
+ if (!currency) throw new Error("DeepSeek API balance row returned an unsupported currency.");
26
+ if (balances.has(currency)) {
27
+ throw new Error(`DeepSeek API balance response repeated ${currency}.`);
28
+ }
29
+ for (const [, label, field] of BALANCE_FIELDS) {
30
+ if (!decimalAmount(balance[field])) {
31
+ throw new Error(`DeepSeek API balance ${label.toLowerCase()} was not a valid amount.`);
32
+ }
33
+ }
34
+ balances.set(currency, balance);
35
+ }
36
+
37
+ const metrics: UsageMetric[] = [
38
+ {
39
+ id: "api-availability",
40
+ label: "API calls",
41
+ value: payload.is_available ? "available" : "unavailable",
42
+ },
43
+ ];
44
+ for (const currency of CURRENCIES) {
45
+ const balance = balances.get(currency);
46
+ if (!balance) continue;
47
+ for (const [id, label, field] of BALANCE_FIELDS) {
48
+ metrics.push({
49
+ id: `${currency.toLowerCase()}-${id}`,
50
+ label,
51
+ value: balance[field] as string,
52
+ unit: "currency",
53
+ currency,
54
+ });
55
+ }
56
+ }
57
+
58
+ return {
59
+ providerId: "deepseek",
60
+ providerName: "DeepSeek",
61
+ capturedAt,
62
+ source: "deepseek-balance",
63
+ semantics: { kind: "api-key", label: "DeepSeek API balance" },
64
+ buckets: [],
65
+ metrics,
66
+ };
67
+ }
68
+
69
+ function asObject(value: unknown): Record<string, unknown> | undefined {
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
71
+ return value as Record<string, unknown>;
72
+ }
73
+
74
+ function deepSeekCurrency(value: unknown): DeepSeekCurrency | undefined {
75
+ return CURRENCIES.find((currency) => currency === value);
76
+ }
77
+
78
+ function decimalAmount(value: unknown): value is string {
79
+ return typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value);
80
+ }