Adds a machine-readable headless surface for live LLM-provider usage and
unifies the gemini-cli quota fetch through one helper, removing the
duplication that existed between usage-bar.js and the new package.
1. snapshotGeminiCliAccount in @singularity-forge/google-gemini-cli-provider
- Single source of truth for { projectId, userTierId, userTierName,
paidTier, models[] } via setupUser + retrieveUserQuota.
- Dedups buckets per modelId, keeping the worst (lowest remainingFraction)
so consumers always see the most-restrictive window. Code Assist
sometimes returns multiple buckets per model; the pessimistic choice
is what every consumer needs.
- discoverGeminiCliModels(cwd?) wraps it for catalog-cache callers that
only need the IDs.
2. sf headless usage subcommand
- New src/headless-usage.ts handler. text (default) and --json output.
Uses the package's snapshot directly — no RPC child, no jiti
gymnastics — matching the shape of headless-uok-status / headless-doctor.
- Wired into src/headless.ts after the doctor block.
- Help text adds the command line.
3. usage-bar.js refactored to delegate
- fetchGeminiUsage no longer imports gemini-cli-core directly. It calls
snapshotGeminiCliAccount and reshapes the result into the existing
{ provider, displayName, windows[] } UI contract.
- Eliminates the duplicate setupUser + retrieveUserQuota code path.
- The fast existsSync(~/.gemini/oauth_creds.json) pre-flight stays
so unauth'd users get a friendly message without paying for OAuth
bootstrap.
4. Model registry refactor (separate track committed alongside)
- src/resources/extensions/sf/model-registry.ts (new) consolidates
canonical model identity, capability tier, and generation tags into
one source of truth that auto-model-selection, benchmark-selector,
and model-router now consume instead of maintaining parallel maps.
All 1487 tests pass (151 files); typecheck clean for both the package
and the SF extensions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
/**
|
|
* headless-usage.ts — `sf headless usage`
|
|
*
|
|
* Purpose: expose live LLM-provider usage data (account tier, project, per-model
|
|
* quota usage with reset windows) via the headless CLI so operators and CI can
|
|
* see capacity state without launching the interactive UI.
|
|
*
|
|
* Today this covers the gemini-cli provider (the most quota-sensitive surface
|
|
* because of AI Ultra's per-model windowed quotas). Other providers can be
|
|
* added by extending the snapshot helper as their introspection APIs are
|
|
* wired into dedicated provider packages.
|
|
*
|
|
* Consumer: headless.ts when command === "usage".
|
|
*/
|
|
|
|
import {
|
|
type GeminiAccountSnapshot,
|
|
snapshotGeminiCliAccount,
|
|
} from "@singularity-forge/google-gemini-cli-provider";
|
|
|
|
export interface HandleUsageOptions {
|
|
json?: boolean;
|
|
}
|
|
|
|
export interface HandleUsageResult {
|
|
exitCode: number;
|
|
}
|
|
|
|
/**
|
|
* Render a snapshot as a compact text table (default) or as JSON for machine
|
|
* consumers. Always writes to stdout; never throws.
|
|
*/
|
|
export async function handleUsage(
|
|
cwd: string,
|
|
options: HandleUsageOptions = {},
|
|
): Promise<HandleUsageResult> {
|
|
let snapshot: GeminiAccountSnapshot | null;
|
|
try {
|
|
snapshot = await snapshotGeminiCliAccount(cwd);
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
const payload = {
|
|
provider: "google-gemini-cli",
|
|
ok: false,
|
|
error: msg,
|
|
};
|
|
process.stdout.write(
|
|
options.json ? `${JSON.stringify(payload)}\n` : `error: ${msg}\n`,
|
|
);
|
|
return { exitCode: 1 };
|
|
}
|
|
|
|
if (!snapshot) {
|
|
const payload = {
|
|
provider: "google-gemini-cli",
|
|
ok: false,
|
|
error:
|
|
"No gemini-cli account snapshot — run `gemini auth login` and verify ~/.gemini/oauth_creds.json exists.",
|
|
};
|
|
process.stdout.write(
|
|
options.json
|
|
? `${JSON.stringify(payload)}\n`
|
|
: `${payload.error}\n`,
|
|
);
|
|
return { exitCode: 1 };
|
|
}
|
|
|
|
if (options.json) {
|
|
process.stdout.write(
|
|
`${JSON.stringify({ provider: "google-gemini-cli", ok: true, snapshot })}\n`,
|
|
);
|
|
return { exitCode: 0 };
|
|
}
|
|
|
|
const lines: string[] = [];
|
|
lines.push("Gemini CLI usage");
|
|
lines.push("");
|
|
lines.push(` project: ${snapshot.projectId}`);
|
|
if (snapshot.userTierId || snapshot.userTierName) {
|
|
lines.push(
|
|
` userTier: ${snapshot.userTierId ?? "?"}${snapshot.userTierName ? ` (${snapshot.userTierName})` : ""}`,
|
|
);
|
|
}
|
|
if (snapshot.paidTier?.id || snapshot.paidTier?.name) {
|
|
lines.push(
|
|
` paidTier: ${snapshot.paidTier.id ?? "?"}${snapshot.paidTier.name ? ` — ${snapshot.paidTier.name}` : ""}`,
|
|
);
|
|
}
|
|
lines.push("");
|
|
lines.push(" Per-model quota:");
|
|
const modelW = Math.max(
|
|
20,
|
|
...snapshot.models.map((m) => m.modelId.length),
|
|
);
|
|
for (const m of snapshot.models) {
|
|
const usedPct = (m.usedFraction * 100).toFixed(1).padStart(5);
|
|
const reset = m.resetTime ?? "-";
|
|
lines.push(` ${m.modelId.padEnd(modelW)} used=${usedPct}% reset=${reset}`);
|
|
}
|
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
return { exitCode: 0 };
|
|
}
|