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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"prettier": "^3.8.0",
"typescript": "^5.8.0",
"typescript": "^5.9.3",
"vitest": "^4.0.17"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
* Validates and normalizes a Google model ID
*/
function isValidGoogleModelId(id: unknown): id is GoogleModelId {
return typeof id === "string" && ["G3PRO", "G3FLASH", "CLAUDE", "G3IMAGE"].includes(id);
return typeof id === "string" && ["G3PRO", "G3FLASH", "CLAUDE", "G3IMAGE", "GPTOSS"].includes(id);
}

function isValidCursorQuotaPlan(plan: unknown): plan is CursorQuotaPlan {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export interface SessionTokensData {
export interface QuotaProviderPresentation {
singleWindowDisplayName?: string;
singleWindowShowRight?: boolean;
/**
* When set to "preserve", the provider's entries are kept individually
* (one per window) even in single-window format styles.
*/
classicStrategy?: "preserve";
}

export interface QuotaProviderResult {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/google-antigravity-companion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ function normalizeCredential(value: unknown): string {
}

function getCompanionResolvePaths(): string[] {
const paths = [...getOpencodeRuntimeDirCandidates().cacheDirs];
const paths = [
...getOpencodeRuntimeDirCandidates().cacheDirs,
];
return paths;
}

Expand Down
39 changes: 35 additions & 4 deletions src/lib/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,15 +454,32 @@ async function fetchGoogleQuota(
return response.json() as Promise<GoogleQuotaResponse>;
}

/**
* Map a modelId to its rate-limit family for local override lookup.
* Families: "claude", "gemini-flash", "gemini-pro", "gpt-oss"
*/
function getRateLimitFamily(
_modelId: GoogleModelId,
modelConfig: (typeof GOOGLE_MODEL_KEYS)[GoogleModelId],
): "claude" | "gemini-flash" | "gemini-pro" | "gpt-oss" | null {
const key = modelConfig.key.toLowerCase();
if (key.includes("claude")) return "claude";
if (key.includes("flash")) return "gemini-flash";
if (key.includes("pro")) return "gemini-pro";
if (key.includes("gpt-oss")) return "gpt-oss";
return null;
}

/**
* Extract model quotas from API response
*/
function extractModelQuotas(
data: GoogleQuotaResponse,
modelIds: GoogleModelId[],
accountEmail?: string,
account: AntigravityAccount,
): GoogleModelQuota[] {
const quotas: GoogleModelQuota[] = [];
const accountEmail = account.email || "Unknown";

for (const modelId of modelIds) {
const modelConfig = GOOGLE_MODEL_KEYS[modelId];
Expand All @@ -480,12 +497,26 @@ function extractModelQuotas(
}

if (modelInfo) {
const remainingFraction = modelInfo.quotaInfo?.remainingFraction ?? 0;
let remainingFraction = modelInfo.quotaInfo?.remainingFraction ?? 0;
let resetTimeIso: string | undefined = modelInfo.quotaInfo?.resetTime;

// Apply local rate limit overrides (rate-limited state from companion plugin)
if (account.rateLimitResetTimes) {
const family = getRateLimitFamily(modelId, modelConfig);
if (family && account.rateLimitResetTimes[family]) {
const resetTimestamp = account.rateLimitResetTimes[family];
if (resetTimestamp && resetTimestamp > Date.now()) {
remainingFraction = 0;
resetTimeIso = new Date(resetTimestamp).toISOString();
}
}
}

quotas.push({
modelId,
displayName: modelConfig.display,
percentRemaining: Math.round(remainingFraction * 100),
resetTimeIso: modelInfo.quotaInfo?.resetTime,
resetTimeIso,
accountEmail,
});
}
Expand Down Expand Up @@ -563,7 +594,7 @@ async function fetchAccountQuotaWithAntigravityRefresh(params: {
throw err;
}
}
const models = extractModelQuotas(data, params.modelIds, email);
const models = extractModelQuotas(data, params.modelIds, params.account);

return { success: true, models, accountEmail: email };
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/grouped-entry-normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function normalizeDurationText(value?: string): string | undefined {

function looksLikeGoogleModel(name: string): boolean {
const lower = name.toLowerCase();
return lower === "claude" || lower === "g3pro" || lower === "g3flash" || lower === "g3image";
return lower === "claude" || lower === "g3pro" || lower === "g3flash" || lower === "g3image" || lower === "gpt-oss";
}

function getGoogleFallbackMeta(name: string): { group: string; label: string } | undefined {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/quota-render-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,14 @@ function normalizeSingleWindowPresentation(
: typeof legacyPresentation.classicShowRight === "boolean"
? legacyPresentation.classicShowRight
: false;
const classicStrategy = legacyPresentation.classicStrategy === "preserve"
? legacyPresentation.classicStrategy
: undefined;

return {
...(singleWindowDisplayName ? { singleWindowDisplayName } : {}),
...(singleWindowShowRight ? { singleWindowShowRight } : {}),
...(classicStrategy ? { classicStrategy } : {}),
};
}

Expand Down Expand Up @@ -421,6 +425,17 @@ function projectProviderResultToStyle(
}

const presentation = normalizeSingleWindowPresentation(result.presentation);
if (presentation?.classicStrategy === "preserve") {
return entries.map((entry) =>
renameSingleWindowEntry(
stripSingleWindowEntryMeta(entry, presentation?.singleWindowShowRight ?? false),
buildSingleWindowName({
entry,
singleWindowDisplayName: entry.group || entry.name,
}),
),
);
}
const selectedEntry = selectSingleWindowEntry(entries);
if (!selectedEntry) {
return [];
Expand Down
2 changes: 1 addition & 1 deletion src/lib/toast-format-grouped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function resolveGroupedRowLabel(entry: QuotaToastEntry): string {
const fromName = extractWindowLabel(entry.name);
if (fromName) return `${fromName} window`;

return "Quota window";
return normalizeLabelText(entry.group) || "Quota window";
}

export function formatQuotaRowsGrouped(params: {
Expand Down
17 changes: 13 additions & 4 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DEFAULT_QUOTA_FORMAT_STYLE } from "./quota-format-style.js";
// =============================================================================

/** Google model identifiers */
export type GoogleModelId = "G3PRO" | "G3FLASH" | "CLAUDE" | "G3IMAGE";
export type GoogleModelId = "G3PRO" | "G3FLASH" | "CLAUDE" | "G3IMAGE" | "GPTOSS";
export type GeminiCliAuthSourceKey =
| "google-gemini-cli"
| "gemini-cli"
Expand Down Expand Up @@ -693,14 +693,23 @@ export const GOOGLE_MODEL_KEYS: Record<
> = {
G3PRO: {
key: "gemini-3.1-pro",
altKey: "gemini-3.1-pro-high|gemini-3.1-pro-low|gemini-3-pro-high|gemini-3-pro-low",
altKey: "gemini-3.1-pro-high|gemini-3.1-pro-low|gemini-3-pro-high|gemini-3-pro-low|gemini-3.5-pro-high|gemini-3.5-pro-low",
display: "G3Pro",
},
G3FLASH: { key: "gemini-3-flash", display: "G3Flash" },
G3FLASH: {
key: "gemini-3-flash",
altKey: "gemini-3-flash-medium|gemini-3-flash-high|gemini-3-flash-low|gemini-3-5-flash-medium|gemini-3-5-flash-high|gemini-3-5-flash-low|gemini-3.5-flash-medium|gemini-3.5-flash-high|gemini-3.5-flash-low",
display: "G3Flash",
},
CLAUDE: {
key: "claude-opus-4-6-thinking",
altKey: "claude-opus-4-5-thinking|claude-opus-4-5",
altKey: "claude-opus-4-5-thinking|claude-opus-4-5|claude-sonnet-4-6|claude-sonnet-4-6-thinking|claude-opus-4-6|gemini-claude-sonnet-4-6|gemini-claude-opus-4-6-thinking",
display: "Claude",
},
G3IMAGE: { key: "gemini-3-pro-image", display: "G3Image" },
GPTOSS: {
key: "gpt-oss-120b-medium",
altKey: "gpt-oss-120b-high|gpt-oss-120b-low|gpt-oss-120b",
display: "GPT-OSS",
},
};
3 changes: 3 additions & 0 deletions src/providers/google-antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const googleAntigravityProvider: QuotaProvider = {
const emailLabel = formatGoogleAccountLabel(m.accountEmail, "fixedGmailHint") || "Antigravity";
return {
name: `${m.displayName} (${emailLabel})`,
group: m.displayName,
label: `${m.displayName}:`,
percentRemaining: m.percentRemaining,
resetTimeIso: m.resetTimeIso,
};
Expand All @@ -59,6 +61,7 @@ export const googleAntigravityProvider: QuotaProvider = {
return attemptedResult(
entries,
formatGoogleAccountErrors(result.errors, "fixedGmailHint"),
{ classicStrategy: "preserve" },
);
},
};
4 changes: 2 additions & 2 deletions tests/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ describe("formatQuotaRows", () => {
entries: [{ name: "Unlabeled Provider", group: "Unlabeled Provider", percentRemaining: 75 }],
});

expect(out).toContain("Quota window");
expect(out).toContain("[Unlabeled Provider]");
});

it("shares single-window provider/window display labels with classic formatting", () => {
Expand Down Expand Up @@ -550,7 +550,7 @@ describe("formatQuotaRows", () => {

expect(out).toContain("[Google Antigravity] (acct)");
expect(out).toContain("\nClaude ");
expect(out).toContain("Quota window");
expect(out).toContain("\nGoogle Antigravity (acct)");
expect(out).not.toContain("[Claude] (acct)");
expect(out).not.toContain("[G3Pro] (acct)");
});
Expand Down
2 changes: 1 addition & 1 deletion tests/lib.google.multi-account-refresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe("google antigravity multi-account refresh", () => {
// First refresh token endpoint call, then quota endpoint call.
const fetchSpy = vi.fn();

// First call: token refresh
// First refresh token endpoint call, then quota endpoint call.
fetchSpy.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ access_token: "new_token", expires_in: 3600 }),
Expand Down
2 changes: 2 additions & 0 deletions tests/providers.google-antigravity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ describe("google antigravity provider", () => {
expect(out.entries).toEqual([
{
name: "Gemini 2.5 Pro (ali..gmail)",
group: "Gemini 2.5 Pro",
label: "Gemini 2.5 Pro:",
percentRemaining: 64,
resetTimeIso: "2026-01-01T00:00:00.000Z",
},
Expand Down
2 changes: 1 addition & 1 deletion tests/tui-sidebar-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe("buildSidebarQuotaPanelLines", () => {
const rendered = lines.join("\n");
expect(rendered).toContain("[Google Antigravity] (acct)");
expect(rendered).toContain("\nClaude ");
expect(rendered).toContain("Quota window");
expect(rendered).toContain("\nGoogle Antigravity (acct)");
expect(rendered).not.toContain("[Claude] (acct)");
expect(rendered).not.toContain("[G3Pro] (acct)");
});
Expand Down
Loading