pi update

This commit is contained in:
Jonas H
2026-04-10 09:01:25 +02:00
parent 7106db51b5
commit 4666776bda
16 changed files with 3042 additions and 35 deletions

View File

@@ -36,8 +36,8 @@ export function writeUsageCache(cache: UsageCache): void {
} catch {}
}
export type ProviderKey = "codex" | "claude" | "zai" | "gemini" | "antigravity";
export type OAuthProviderId = "openai-codex" | "anthropic" | "google-gemini-cli" | "google-antigravity";
export type ProviderKey = "codex" | "claude" | "zai" | "gemini" | "antigravity" | "opencode-go";
export type OAuthProviderId = "openai-codex" | "anthropic" | "google-gemini-cli" | "google-antigravity" | "opencode-go";
export interface AuthData {
"openai-codex"?: { access?: string; refresh?: string; expires?: number };
@@ -45,6 +45,7 @@ export interface AuthData {
zai?: { key?: string; access?: string; refresh?: string; expires?: number };
"google-gemini-cli"?: { access?: string; refresh?: string; projectId?: string; expires?: number };
"google-antigravity"?: { access?: string; refresh?: string; projectId?: string; expires?: number };
"opencode-go"?: { key?: string; access?: string };
}
export interface UsageData {
@@ -61,6 +62,127 @@ export interface UsageData {
export type UsageByProvider = Record<ProviderKey, UsageData | null>;
// OpenCode Go usage tracking (local, since no API exists yet)
export interface OpenCodeGoLocalUsage {
/** Dollar value used in the 5-hour window */
fiveHourUsed: number;
/** Dollar value used in the weekly window */
weeklyUsed: number;
/** Dollar value used in the monthly window */
monthlyUsed: number;
/** Timestamp of the last update */
lastUpdated: number;
/** When the current 5-hour window started */
fiveHourWindowStart: number;
/** When the current week started (Unix ms) */
weekStart: number;
/** When the current month started (Unix ms) */
monthStart: number;
}
const OPENCODE_GO_USAGE_FILE = path.join(os.homedir(), ".pi", "agent", "opencode-go-usage.json");
const OPENCODE_GO_FIVE_HOUR_LIMIT = 12;
const OPENCODE_GO_WEEKLY_LIMIT = 30;
const OPENCODE_GO_MONTHLY_LIMIT = 60;
const OPENCODE_GO_FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
export function readOpenCodeGoUsage(): OpenCodeGoLocalUsage | null {
try {
const raw = fs.readFileSync(OPENCODE_GO_USAGE_FILE, "utf-8");
const parsed = JSON.parse(raw);
if (typeof parsed?.lastUpdated === "number") return parsed as OpenCodeGoLocalUsage;
} catch {}
return null;
}
export function writeOpenCodeGoUsage(usage: OpenCodeGoLocalUsage): void {
try {
const dir = path.dirname(OPENCODE_GO_USAGE_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const tmp = `${OPENCODE_GO_USAGE_FILE}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
fs.renameSync(tmp, OPENCODE_GO_USAGE_FILE);
} catch {}
}
export function resetOpenCodeGoUsageIfNeeded(existing: OpenCodeGoLocalUsage | null): OpenCodeGoLocalUsage {
const now = Date.now();
const nowDate = new Date(now);
// Start with defaults
let usage: OpenCodeGoLocalUsage = existing ?? {
fiveHourUsed: 0,
weeklyUsed: 0,
monthlyUsed: 0,
lastUpdated: now,
fiveHourWindowStart: now,
weekStart: now,
monthStart: now,
};
// Reset 5-hour window if expired
if (now - usage.fiveHourWindowStart >= OPENCODE_GO_FIVE_HOUR_MS) {
usage.fiveHourUsed = 0;
usage.fiveHourWindowStart = now;
}
// Reset weekly window (Monday-based)
const dayOfWeek = nowDate.getDay();
const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const thisMonday = new Date(nowDate);
thisMonday.setDate(nowDate.getDate() - daysSinceMonday);
thisMonday.setHours(0, 0, 0, 0);
if (usage.weekStart < thisMonday.getTime()) {
usage.weeklyUsed = 0;
usage.weekStart = thisMonday.getTime();
}
// Reset monthly window (1st of month)
const thisMonthStart = new Date(nowDate.getFullYear(), nowDate.getMonth(), 1);
if (usage.monthStart < thisMonthStart.getTime()) {
usage.monthlyUsed = 0;
usage.monthStart = thisMonthStart.getTime();
}
usage.lastUpdated = now;
return usage;
}
export function addOpenCodeGoSpend(dollars: number): void {
let usage = resetOpenCodeGoUsageIfNeeded(readOpenCodeGoUsage());
usage.fiveHourUsed += dollars;
usage.weeklyUsed += dollars;
usage.monthlyUsed += dollars;
usage.lastUpdated = Date.now();
writeOpenCodeGoUsage(usage);
}
export function getOpenCodeGoUsageData(): UsageData {
const usage = resetOpenCodeGoUsageIfNeeded(readOpenCodeGoUsage());
if (!usage) {
return { session: 0, weekly: 0, error: "no local usage data" };
}
const sessionPct = Math.min(100, (usage.fiveHourUsed / OPENCODE_GO_FIVE_HOUR_LIMIT) * 100);
const weeklyPct = Math.min(100, (usage.weeklyUsed / OPENCODE_GO_WEEKLY_LIMIT) * 100);
// Calculate resets
const fiveHourEnd = usage.fiveHourWindowStart + OPENCODE_GO_FIVE_HOUR_MS;
const fiveHourRemaining = Math.max(0, fiveHourEnd - Date.now());
const weekEnd = usage.weekStart + 7 * 24 * 60 * 60 * 1000;
const weekRemaining = Math.max(0, weekEnd - Date.now());
return {
session: sessionPct,
weekly: weeklyPct,
sessionResetsIn: formatDuration(Math.round(fiveHourRemaining / 1000)),
weeklyResetsIn: formatDuration(Math.round(weekRemaining / 1000)),
extraSpend: usage.monthlyUsed,
extraLimit: OPENCODE_GO_MONTHLY_LIMIT,
};
}
export interface UsageEndpoints {
zai: string;
gemini: string;
@@ -600,12 +722,14 @@ export function detectProvider(
if (typeof model === "string") return null;
const provider = (model.provider || "").toLowerCase();
const id = (model.id || "").toLowerCase();
if (provider === "openai-codex") return "codex";
if (provider === "anthropic") return "claude";
if (provider === "zai") return "zai";
if (provider === "google-gemini-cli") return "gemini";
if (provider === "google-antigravity") return "antigravity";
if (provider === "opencode-go" || id.startsWith("opencode-go/")) return "opencode-go";
return null;
}
@@ -615,6 +739,7 @@ export function providerToOAuthProviderId(active: ProviderKey | null): OAuthProv
if (active === "claude") return "anthropic";
if (active === "gemini") return "google-gemini-cli";
if (active === "antigravity") return "google-antigravity";
if (active === "opencode-go") return "opencode-go";
return null;
}
@@ -629,6 +754,9 @@ export function canShowForProvider(active: ProviderKey | null, auth: AuthData |
if (active === "antigravity") {
return !!(auth["google-antigravity"]?.access || auth["google-antigravity"]?.refresh) && !!endpoints.antigravity;
}
if (active === "opencode-go") {
return !!(auth["opencode-go"]?.key || auth["opencode-go"]?.access);
}
return false;
}
@@ -654,6 +782,7 @@ export async function fetchAllUsages(config: FetchAllUsagesConfig = {}): Promise
zai: null,
gemini: null,
antigravity: null,
"opencode-go": null,
};
if (!auth) return results;
@@ -733,6 +862,11 @@ export async function fetchAllUsages(config: FetchAllUsagesConfig = {}): Promise
}
}
// OpenCode Go uses local tracking (no public API yet)
if (authData["opencode-go"]?.key || authData["opencode-go"]?.access) {
results["opencode-go"] = getOpenCodeGoUsageData();
}
await Promise.all(tasks);
return results;
}

View File

@@ -79,6 +79,7 @@ const PROVIDER_LABELS: Record<ProviderKey, string> = {
zai: "Z.AI",
gemini: "Gemini",
antigravity: "Antigravity",
"opencode-go": "Go",
};
// ---------------------------------------------------------------------------
@@ -163,6 +164,7 @@ class UsageSelectorComponent extends Container implements Focusable {
{ key: "zai", name: "Z.AI" },
{ key: "gemini", name: "Gemini" },
{ key: "antigravity", name: "Antigravity" },
{ key: "opencode-go", name: "Go" },
];
this.allItems = [];
for (const p of providers) {
@@ -287,7 +289,7 @@ interface PollOptions {
export default function (pi: ExtensionAPI) {
const endpoints = resolveUsageEndpoints();
const state: UsageState = {
codex: null, claude: null, zai: null, gemini: null, antigravity: null,
codex: null, claude: null, zai: null, gemini: null, antigravity: null, "opencode-go": null,
lastPoll: 0, activeProvider: null,
};
@@ -433,6 +435,10 @@ export default function (pi: ExtensionAPI) {
result = creds?.access
? await fetchGoogleUsage(creds.access, endpoints.gemini, creds.projectId, "gemini", { endpoints })
: { session: 0, weekly: 0, error: "missing access token (try /login again)" };
} else if (active === "opencode-go") {
// OpenCode Go uses local tracking (no public usage API yet)
const { getOpenCodeGoUsageData } = await import("./core");
result = getOpenCodeGoUsageData();
} else {
const creds = effectiveAuth["google-antigravity"];
result = creds?.access
@@ -561,6 +567,22 @@ export default function (pi: ExtensionAPI) {
void poll({ forceFresh: true });
});
// Listen for OpenCode Go spend events from other extensions
pi.events.on("opencode-go:spend", async (amount: number) => {
if (typeof amount === "number" && amount > 0) {
const { addOpenCodeGoSpend } = await import("./core");
addOpenCodeGoSpend(amount);
// Invalidate cache and re-poll
const cache = readUsageCache();
if (cache?.data?.["opencode-go"]) {
const nextCache: import("./core").UsageCache = { ...cache, data: { ...cache.data } };
delete nextCache.data["opencode-go"];
writeUsageCache(nextCache);
}
void poll({ forceFresh: true });
}
});
// ── /usage command ───────────────────────────────────────────────────────
pi.registerCommand("usage", {