singularity-forge/src/headless-usage.ts
Mikael Hugo 383e495085 feat(headless,gemini-cli): add sf headless usage + unify gemini quota path
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>
2026-05-14 03:42:53 +02:00

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 };
}