Skip to content
Open
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
56 changes: 37 additions & 19 deletions commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ import { getAgentSettingsPath } from "pi-provider-utils/agent-paths";
import { normalizeUnknownError } from "pi-provider-utils/streams";
import type { AccountManager } from "./account-manager";
import { openLoginInBrowser } from "./browser";
import type { createUsageStatusController } from "./status";
import {
formatUsageSummaryText,
loadFooterPreferences,
type PercentDisplayMode,
type createUsageStatusController,
} from "./status";
import { type Account, STORAGE_FILE } from "./storage";
import { formatResetAt, isUsageUntouched } from "./usage";
import { isUsageUntouched } from "./usage";

const SETTINGS_FILE = getAgentSettingsPath();
const NO_ACCOUNTS_MESSAGE =
Expand Down Expand Up @@ -116,28 +121,32 @@ function getAccountTags(
function formatUsageSummary(
accountManager: AccountManager,
account: Account,
usageMode: PercentDisplayMode,
): string {
const usage = accountManager.getCachedUsage(account.email);
const primaryUsed = usage?.primary?.usedPercent;
const secondaryUsed = usage?.secondary?.usedPercent;
const primaryReset = usage?.primary?.resetAt;
const secondaryReset = usage?.secondary?.resetAt;
const primaryLabel =
primaryUsed === undefined ? "unknown" : `${Math.round(primaryUsed)}%`;
const secondaryLabel =
secondaryUsed === undefined ? "unknown" : `${Math.round(secondaryUsed)}%`;
return `5h ${primaryLabel} reset:${formatResetAt(primaryReset)} | weekly ${secondaryLabel} reset:${formatResetAt(secondaryReset)}`;
return formatUsageSummaryText(
accountManager.getCachedUsage(account.email),
usageMode,
);
}

function formatAccountStatusLine(
accountManager: AccountManager,
email: string,
usageMode: PercentDisplayMode,
): string {
const account = accountManager.getAccount(email);
if (!account) return email;
const tags = getAccountTags(accountManager, account).join(", ");
const suffix = tags ? ` (${tags})` : "";
return `${account.email}${suffix} - ${formatUsageSummary(accountManager, account)}`;
return `${account.email}${suffix} - ${formatUsageSummary(accountManager, account, usageMode)}`;
}

async function loadUsageMode(): Promise<PercentDisplayMode> {
try {
return (await loadFooterPreferences()).usageMode;
} catch {
return "left";
}
}

function getSubcommandCompletions(prefix: string): AutocompleteItem[] | null {
Expand Down Expand Up @@ -278,6 +287,7 @@ async function refreshSingleAccount(
ctx: ExtensionCommandContext,
accountManager: AccountManager,
email: string,
usageMode: PercentDisplayMode,
): Promise<void> {
const account = accountManager.getAccount(email);
if (!account) {
Expand All @@ -297,7 +307,7 @@ async function refreshSingleAccount(

await accountManager.refreshUsageForAccount(account, { force: true });
ctx.ui.notify(
`refreshed ${formatAccountStatusLine(accountManager, email)}`,
`refreshed ${formatAccountStatusLine(accountManager, email, usageMode)}`,
"info",
);
}
Expand Down Expand Up @@ -344,6 +354,7 @@ async function promptForNewAccountIdentifier(
async function openAccountManagementPanel(
ctx: ExtensionCommandContext,
accountManager: AccountManager,
usageMode: PercentDisplayMode,
): Promise<AccountPanelResult> {
const accounts = accountManager.getAccounts();

Expand Down Expand Up @@ -405,7 +416,7 @@ async function openAccountManagementPanel(
: "dim";
const secondary = theme.fg(
summaryColor,
formatUsageSummary(accountManager, account),
formatUsageSummary(accountManager, account, usageMode),
);
return [primary, truncateToWidth(` ${secondary}`, width, "")];
}
Expand Down Expand Up @@ -585,6 +596,7 @@ async function openAccountManagementFlow(
accountManager: AccountManager,
statusController: ReturnType<typeof createUsageStatusController>,
): Promise<void> {
const usageMode = await loadUsageMode();
while (true) {
const accounts = accountManager.getAccounts();
if (accounts.length === 0) {
Expand All @@ -595,7 +607,7 @@ async function openAccountManagementFlow(
continue;
}

const result = await openAccountManagementPanel(ctx, accountManager);
const result = await openAccountManagementPanel(ctx, accountManager, usageMode);
if (!result) return;

if (result.action === "add") {
Expand All @@ -613,7 +625,7 @@ async function openAccountManagementFlow(
}

if (result.action === "refresh") {
await refreshSingleAccount(ctx, accountManager, result.email);
await refreshSingleAccount(ctx, accountManager, result.email, usageMode);
await statusController.refreshFor(ctx);
continue;
}
Expand Down Expand Up @@ -669,8 +681,9 @@ async function runAccountsSubcommand(
}

if (!ctx.hasUI) {
const usageMode = await loadUsageMode();
const lines = accounts.map((account) =>
formatAccountStatusLine(accountManager, account.email),
formatAccountStatusLine(accountManager, account.email, usageMode),
);
ctx.ui.notify(lines.join("\n"), "info");
return;
Expand Down Expand Up @@ -872,7 +885,12 @@ async function runRefreshSubcommand(
await openAccountManagementFlow(pi, ctx, accountManager, statusController);
return;
}
await refreshSingleAccount(ctx, accountManager, rest);
await refreshSingleAccount(
ctx,
accountManager,
rest,
await loadUsageMode(),
);
await statusController.refreshFor(ctx);
}

Expand Down
86 changes: 77 additions & 9 deletions provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { getApiProvider } from "@mariozechner/pi-ai";
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { mirrorProvider } from "pi-provider-utils/providers";
import type { AccountManager } from "./account-manager";
import { createStreamWrapper } from "./stream-wrapper";
Expand All @@ -20,6 +23,65 @@ export interface ProviderModelDef {
maxTokens: number;
}

type OpenAICodexModelOverride = Partial<
Pick<
ProviderModelDef,
"name" | "reasoning" | "input" | "cost" | "contextWindow" | "maxTokens"
>
>;

function getAgentDir(): string {
return process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
}

function readOpenAICodexModelOverrides(): Map<string, OpenAICodexModelOverride> {
const modelsJsonPath = join(getAgentDir(), "models.json");
if (!existsSync(modelsJsonPath)) {
return new Map();
}
try {
const parsed = JSON.parse(readFileSync(modelsJsonPath, "utf-8")) as {
providers?: {
"openai-codex"?: {
modelOverrides?: Record<string, OpenAICodexModelOverride>;
};
};
};
return new Map(
Object.entries(
parsed.providers?.["openai-codex"]?.modelOverrides ?? {},
),
);
} catch {
return new Map();
}
}

function applyOpenAICodexOverride(
model: ProviderModelDef,
override: OpenAICodexModelOverride | undefined,
): ProviderModelDef {
if (!override) {
return model;
}
return {
...model,
name: override.name ?? model.name,
reasoning: override.reasoning ?? model.reasoning,
input: override.input ? [...override.input] : model.input,
cost: override.cost
? {
input: override.cost.input ?? model.cost.input,
output: override.cost.output ?? model.cost.output,
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
}
: model.cost,
contextWindow: override.contextWindow ?? model.contextWindow,
maxTokens: override.maxTokens ?? model.maxTokens,
};
}

export function getOpenAICodexMirror(): {
baseUrl: string;
models: ProviderModelDef[];
Expand All @@ -28,17 +90,23 @@ export function getOpenAICodexMirror(): {
if (!mirror) {
return { baseUrl: "https://chatgpt.com/backend-api", models: [] };
}
const overrides = readOpenAICodexModelOverrides();
return {
baseUrl: mirror.baseUrl,
models: mirror.models.map((m) => ({
id: m.id,
name: m.name,
reasoning: m.reasoning,
input: [...m.input],
cost: { ...m.cost },
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
})),
models: mirror.models.map((m) =>
applyOpenAICodexOverride(
{
id: m.id,
name: m.name,
reasoning: m.reasoning,
input: [...m.input],
cost: { ...m.cost },
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
},
overrides.get(m.id),
),
),
};
}

Expand Down
16 changes: 16 additions & 0 deletions status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,22 @@ function formatPercent(
return `${Math.round(clampPercent(displayPercent))}% ${mode}`;
}

export function formatUsageSummaryText(
usage: CodexUsageSnapshot | undefined,
mode: PercentDisplayMode = "left",
): string {
const primaryDisplay = usedToDisplayPercent(usage?.primary?.usedPercent, mode);
const secondaryDisplay = usedToDisplayPercent(
usage?.secondary?.usedPercent,
mode,
);
const primaryLabel =
primaryDisplay === undefined ? "unknown" : formatPercent(primaryDisplay, mode);
const secondaryLabel =
secondaryDisplay === undefined ? "unknown" : formatPercent(secondaryDisplay, mode);
return `5h ${primaryLabel} reset:${formatResetAt(usage?.primary?.resetAt)} | weekly ${secondaryLabel} reset:${formatResetAt(usage?.secondary?.resetAt)}`;
}

function formatResetCountdown(resetAt: number | undefined): string | undefined {
if (typeof resetAt !== "number" || Number.isNaN(resetAt)) return undefined;
const totalSeconds = Math.max(0, Math.round((resetAt - Date.now()) / 1000));
Expand Down