singularity-forge/src/headless-usage.ts

103 lines
2.9 KiB
TypeScript
Raw Normal View History

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