Skip to content

Instantly share code, notes, and snippets.

@elbruno
Last active June 20, 2026 08:20
Show Gist options
  • Select an option

  • Save elbruno/43fa7650da08f976d6347492c8af9241 to your computer and use it in GitHub Desktop.

Select an option

Save elbruno/43fa7650da08f976d6347492c8af9241 to your computer and use it in GitHub Desktop.
AI-credit monitor for GitHub Copilot App
{
"name": "copilot-app-cost",
"version": 1
}
// .github/extensions/copilot-app-cost/extension.mjs
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
// .github/extensions/copilot-app-cost/lib/cost.mjs
var AI_CREDIT_USD = 0.01;
var NANO_AI_UNITS_PER_AI_CREDIT = 1e9;
var TOKENS_PER_MILLION = 1e6;
var PLAN_ALLOTMENTS = Object.freeze({
free: { baseAiCredits: 0, flexAiCredits: 0, promotionalAiCredits: 0, totalAiCredits: 0, pooled: false },
pro: { baseAiCredits: 1e3, flexAiCredits: 500, promotionalAiCredits: 0, totalAiCredits: 1500, pooled: false },
"pro-plus": { baseAiCredits: 3900, flexAiCredits: 3100, promotionalAiCredits: 0, totalAiCredits: 7e3, pooled: false },
max: { baseAiCredits: 1e4, flexAiCredits: 1e4, promotionalAiCredits: 0, totalAiCredits: 2e4, pooled: false },
business: { baseAiCredits: 1900, flexAiCredits: 0, promotionalAiCredits: 1100, totalAiCredits: 3e3, pooled: true },
enterprise: { baseAiCredits: 3900, flexAiCredits: 0, promotionalAiCredits: 3100, totalAiCredits: 7e3, pooled: true },
student: { baseAiCredits: 0, flexAiCredits: 0, promotionalAiCredits: 0, totalAiCredits: 0, pooled: false }
});
var MODEL_ALIASES = Object.freeze({
"gpt-5 mini": "gpt-5-mini",
"gpt-5.4 mini": "gpt-5.4-mini",
"gpt-5.4 nano": "gpt-5.4-nano",
"claude haiku 4.5": "claude-haiku-4.5",
"claude sonnet 4": "claude-sonnet-4",
"claude sonnet 4.5": "claude-sonnet-4.5",
"claude sonnet 4.6": "claude-sonnet-4.6",
"claude opus 4.5": "claude-opus-4.5",
"claude opus 4.6": "claude-opus-4.6",
"claude opus 4.7": "claude-opus-4.7",
"claude opus 4.8": "claude-opus-4.8",
"claude fable 5": "claude-fable-5",
"gemini 2.5 pro": "gemini-2.5-pro",
"gemini 3 flash": "gemini-3-flash",
"gemini 3.1 pro": "gemini-3.1-pro",
"gemini 3.5 flash": "gemini-3.5-flash",
goldeneye: "mai-code-1-flash",
"mai code 1 flash": "mai-code-1-flash",
"raptor mini": "raptor-mini"
});
var MODEL_RATES = Object.freeze({
"gpt-5-mini": rate({ input: 0.25, cachedInput: 0.025, output: 2 }),
"gpt-5.3-codex": rate({ input: 1.75, cachedInput: 0.175, output: 14 }),
"gpt-5.4": rate({ input: 2.5, cachedInput: 0.25, output: 15, longContext: { thresholdInputTokens: 272e3, input: 5, cachedInput: 0.5, output: 22.5 } }),
"gpt-5.4-mini": rate({ input: 0.75, cachedInput: 0.075, output: 4.5 }),
"gpt-5.4-nano": rate({ input: 0.2, cachedInput: 0.02, output: 1.25 }),
"gpt-5.5": rate({ input: 5, cachedInput: 0.5, output: 30, longContext: { thresholdInputTokens: 272e3, input: 10, cachedInput: 1, output: 45 } }),
"claude-haiku-4.5": rate({ input: 1, cachedInput: 0.1, cacheWrite: 1.25, output: 5 }),
"claude-sonnet-4": rate({ input: 3, cachedInput: 0.3, cacheWrite: 3.75, output: 15 }),
"claude-sonnet-4.5": rate({ input: 3, cachedInput: 0.3, cacheWrite: 3.75, output: 15 }),
"claude-sonnet-4.6": rate({ input: 3, cachedInput: 0.3, cacheWrite: 3.75, output: 15 }),
"claude-opus-4.5": rate({ input: 5, cachedInput: 0.5, cacheWrite: 6.25, output: 25 }),
"claude-opus-4.6": rate({ input: 5, cachedInput: 0.5, cacheWrite: 6.25, output: 25 }),
"claude-opus-4.7": rate({ input: 5, cachedInput: 0.5, cacheWrite: 6.25, output: 25 }),
"claude-opus-4.8": rate({ input: 5, cachedInput: 0.5, cacheWrite: 6.25, output: 25 }),
"claude-fable-5": rate({ input: 10, cachedInput: 1, cacheWrite: 12.5, output: 50 }),
"gemini-2.5-pro": rate({ input: 1.25, cachedInput: 0.125, output: 10 }),
"gemini-3-flash": rate({ input: 0.5, cachedInput: 0.05, output: 3 }),
"gemini-3.1-pro": rate({ input: 2, cachedInput: 0.2, output: 12, longContext: { thresholdInputTokens: 2e5, input: 4, cachedInput: 0.4, output: 18 } }),
"gemini-3.5-flash": rate({ input: 1.5, cachedInput: 0.15, output: 9 }),
"mai-code-1-flash": rate({ input: 0.75, cachedInput: 0.075, output: 4.5 }),
"raptor-mini": rate({ input: 0.25, cachedInput: 0.025, output: 2 })
});
function normalizePlanId(plan) {
const normalized = String(plan ?? "pro").trim().toLowerCase().replace(/[_\s]+/g, "-");
if (["pro+", "pro-plus", "proplus", "copilot-pro+", "copilot-pro-plus", "github-copilot-pro-plus"].includes(normalized)) {
return "pro-plus";
}
if (["copilot-max", "github-copilot-max"].includes(normalized)) {
return "max";
}
if (["individual", "copilot-pro", "github-copilot-pro"].includes(normalized)) {
return "pro";
}
if (normalized === "enterprise-cloud") {
return "enterprise";
}
return normalized;
}
function getPlanAllowance(plan) {
return PLAN_ALLOTMENTS[normalizePlanId(plan)] ?? PLAN_ALLOTMENTS.pro;
}
function metricsToSessionUsage(sessionId, metrics, metadata = {}) {
const modelMetrics = metrics?.modelMetrics && typeof metrics.modelMetrics === "object" ? metrics.modelMetrics : {};
return {
sessionId,
source: metadata.source ?? "session.rpc.usage.getMetrics",
metricsTimestamp: metadata.metricsTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
currentModel: metrics?.currentModel ?? null,
totalNanoAiu: readOptionalNumber(metrics?.totalNanoAiu),
totalUserRequests: readOptionalNumber(metrics?.totalUserRequests),
totalPremiumRequestCost: readOptionalNumber(metrics?.totalPremiumRequestCost),
totalApiDurationMs: readOptionalNumber(metrics?.totalApiDurationMs),
sessionStartTime: metrics?.sessionStartTime ?? null,
lastCallInputTokens: readOptionalNumber(metrics?.lastCallInputTokens),
lastCallOutputTokens: readOptionalNumber(metrics?.lastCallOutputTokens),
codeChanges: {
linesAdded: readOptionalNumber(metrics?.codeChanges?.linesAdded),
linesRemoved: readOptionalNumber(metrics?.codeChanges?.linesRemoved),
filesModifiedCount: readOptionalNumber(metrics?.codeChanges?.filesModifiedCount)
},
modelUsage: Object.entries(modelMetrics).map(([model, item]) => ({
model,
requests: readOptionalNumber(item?.requests?.count) ?? 0,
inputTokens: readOptionalNumber(item?.usage?.inputTokens) ?? 0,
cachedInputTokens: readOptionalNumber(item?.usage?.cacheReadTokens) ?? 0,
cacheWriteTokens: readOptionalNumber(item?.usage?.cacheWriteTokens) ?? 0,
outputTokens: readOptionalNumber(item?.usage?.outputTokens) ?? 0,
reasoningTokens: readOptionalNumber(item?.usage?.reasoningTokens) ?? 0,
totalNanoAiu: readOptionalNumber(item?.totalNanoAiu)
}))
};
}
function calculateSessionEstimate(sessionUsage, options = {}) {
const plan = normalizePlanId(options.plan ?? sessionUsage?.plan ?? "pro");
const billReasoningTokens = options.billReasoningTokens === true;
const modelBreakdown = (sessionUsage?.modelUsage ?? []).map((usage) => {
const modelId = resolveKnownModelId(usage.model);
const rateEntry = MODEL_RATES[modelId];
const rate2 = selectRate(rateEntry, usage);
const directAiCredits = readDirectAiCredits(usage, "copilot-model-aiu");
const uncachedInputTokens = Math.max(numberOrZero(usage.inputTokens) - numberOrZero(usage.cachedInputTokens), 0);
const inputUsd = rate2 ? costForTokens(uncachedInputTokens, rate2.inputPerMillionUsd) : 0;
const cachedInputUsd = rate2 ? costForTokens(usage.cachedInputTokens, rate2.cachedInputPerMillionUsd) : 0;
const cacheWriteUsd = rate2 ? costForTokens(usage.cacheWriteTokens, rate2.cacheWritePerMillionUsd) : 0;
const outputUsd = rate2 ? costForTokens(usage.outputTokens, rate2.outputPerMillionUsd) : 0;
const reasoningUsd = rate2 && billReasoningTokens ? costForTokens(usage.reasoningTokens, rate2.outputPerMillionUsd) : 0;
const tokenEstimatedTotalUsd = round(inputUsd + cachedInputUsd + cacheWriteUsd + outputUsd + reasoningUsd);
const aiCredits2 = directAiCredits?.aiCredits ?? usdToAiCredits(tokenEstimatedTotalUsd);
const totalUsd2 = directAiCredits ? aiCreditsToUsd(directAiCredits.aiCredits) : tokenEstimatedTotalUsd;
return {
model: modelId,
displayName: usage.model,
requests: numberOrZero(usage.requests),
inputTokens: numberOrZero(usage.inputTokens),
cachedInputTokens: numberOrZero(usage.cachedInputTokens),
cacheWriteTokens: numberOrZero(usage.cacheWriteTokens),
outputTokens: numberOrZero(usage.outputTokens),
reasoningTokens: numberOrZero(usage.reasoningTokens),
uncachedInputTokens,
totalNanoAiu: directAiCredits?.totalNanoAiu,
totalUsd: totalUsd2,
aiCredits: aiCredits2,
rateTier: rate2?.tier ?? null,
creditCalculationMethod: directAiCredits ? "copilot-aiu" : "token-estimate",
creditCalculationSource: directAiCredits?.source ?? "token-rate-estimate"
};
});
const directSessionAiCredits = readDirectAiCredits(sessionUsage, "copilot-session-aiu");
const totalUsd = directSessionAiCredits ? aiCreditsToUsd(directSessionAiCredits.aiCredits) : round(sum(modelBreakdown.map((item) => item.totalUsd)));
const aiCredits = directSessionAiCredits?.aiCredits ?? usdToAiCredits(totalUsd);
const allowance = getPlanAllowance(plan);
return {
plan,
currency: "USD",
totalUsd,
aiCredits,
totalNanoAiu: directSessionAiCredits?.totalNanoAiu ?? readOptionalNumber(sessionUsage?.totalNanoAiu),
includedAiCredits: allowance.totalAiCredits,
allowanceUsagePercentage: allowance.totalAiCredits > 0 ? round(aiCredits / allowance.totalAiCredits * 100) : null,
pooledAllowance: allowance.pooled,
calculationMethod: directSessionAiCredits ? "copilot-aiu" : "token-estimate",
calculationSource: directSessionAiCredits?.source ?? getBreakdownMethod(modelBreakdown),
billReasoningTokens,
modelBreakdown
};
}
function resolveKnownModelId(model) {
const raw = String(model ?? "").trim();
const normalized = MODEL_ALIASES[raw.toLowerCase()] ?? raw.toLowerCase().replace(/\s+/g, "-");
if (MODEL_RATES[normalized]) {
return normalized;
}
return Object.keys(MODEL_RATES).filter((candidate) => normalized.startsWith(candidate)).sort((a, b) => b.length - a.length)[0] ?? normalized;
}
function selectRate(rateEntry, usage) {
if (!rateEntry) {
return void 0;
}
const threshold = numberOrZero(rateEntry.longContext?.thresholdInputTokens);
if (rateEntry.longContext && threshold > 0 && numberOrZero(usage.inputTokens) > threshold) {
return rateEntry.longContext;
}
return rateEntry;
}
function getBreakdownMethod(modelBreakdown) {
if (modelBreakdown.length === 0) {
return "token-rate-estimate";
}
const aiuCount = modelBreakdown.filter((item) => item.creditCalculationMethod === "copilot-aiu").length;
if (aiuCount === modelBreakdown.length) {
return "copilot-model-aiu";
}
if (aiuCount > 0) {
return "mixed-model-aiu-token-estimate";
}
return "token-rate-estimate";
}
function readDirectAiCredits(value, source) {
const totalNanoAiu = readOptionalNumber(value?.totalNanoAiu);
if (totalNanoAiu !== void 0) {
return {
aiCredits: round(totalNanoAiu / NANO_AI_UNITS_PER_AI_CREDIT),
source,
totalNanoAiu
};
}
return void 0;
}
function costForTokens(tokens, perMillionUsd) {
return round(numberOrZero(tokens) / TOKENS_PER_MILLION * numberOrZero(perMillionUsd));
}
function usdToAiCredits(usd) {
return round(numberOrZero(usd) / AI_CREDIT_USD);
}
function aiCreditsToUsd(aiCredits) {
return round(numberOrZero(aiCredits) * AI_CREDIT_USD);
}
function sum(values) {
return values.reduce((total, value) => total + numberOrZero(value), 0);
}
function numberOrZero(value) {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
}
function readOptionalNumber(value) {
if (value === void 0 || value === null || value === "") {
return void 0;
}
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
}
function round(value) {
return Math.round((Number(value ?? 0) + Number.EPSILON) * 1e6) / 1e6;
}
function rate({ input, cachedInput, cacheWrite = 0, output, longContext }) {
const base = {
tier: "default",
inputPerMillionUsd: input,
cachedInputPerMillionUsd: cachedInput,
cacheWritePerMillionUsd: cacheWrite,
outputPerMillionUsd: output
};
if (!longContext) {
return base;
}
return {
...base,
longContext: {
tier: "long-context",
thresholdInputTokens: longContext.thresholdInputTokens,
inputPerMillionUsd: longContext.input,
cachedInputPerMillionUsd: longContext.cachedInput,
cacheWritePerMillionUsd: longContext.cacheWrite ?? cacheWrite,
outputPerMillionUsd: longContext.output
}
};
}
// .github/extensions/copilot-app-cost/lib/history.mjs
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function getSessionStateDirectory(options = {}) {
const copilotHome = options.copilotHome ?? process.env.COPILOT_HOME ?? path.join(os.homedir(), ".copilot");
return path.join(copilotHome, "session-state");
}
function listRecentSessionUsages(currentSessionId, options = {}) {
const directory = getSessionStateDirectory(options);
if (!fs.existsSync(directory)) {
return [];
}
const candidates = fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
const sessionId = entry.name;
const eventsPath = path.join(directory, sessionId, "events.jsonl");
if (!fs.existsSync(eventsPath)) {
return null;
}
return {
sessionId,
eventsPath,
updatedAtMs: fs.statSync(eventsPath).mtimeMs
};
}).filter(Boolean).sort((left, right) => right.updatedAtMs - left.updatedAtMs).slice(0, options.limit ?? 20);
return candidates.map((candidate) => readSessionUsageFromEvents(candidate.sessionId, { ...options, eventsPath: candidate.eventsPath, currentSessionId })).filter(Boolean).sort((left, right) => Date.parse(right.updatedAt ?? "") - Date.parse(left.updatedAt ?? ""));
}
function readSessionUsageFromEvents(sessionId, options = {}) {
const eventsPath = options.eventsPath ?? path.join(getSessionStateDirectory(options), sessionId, "events.jsonl");
if (!fs.existsSync(eventsPath)) {
return null;
}
const events = readSessionEvents(eventsPath);
const event = events.richestMetricsEvent ?? events.latestMetricsEvent;
if (!event?.data?.modelMetrics && event?.data?.totalNanoAiu === void 0) {
return null;
}
const workspace = readSessionWorkspaceMetadata(sessionId, options);
const modelUsage = Object.entries(event.data.modelMetrics ?? {}).map(([model, metrics]) => ({
model,
requests: numberOrZero2(metrics?.requests?.count),
inputTokens: numberOrZero2(metrics?.usage?.inputTokens),
cachedInputTokens: numberOrZero2(metrics?.usage?.cacheReadTokens),
cacheWriteTokens: numberOrZero2(metrics?.usage?.cacheWriteTokens),
outputTokens: numberOrZero2(metrics?.usage?.outputTokens),
reasoningTokens: numberOrZero2(metrics?.usage?.reasoningTokens),
totalNanoAiu: readOptionalNumber2(metrics?.totalNanoAiu)
}));
return {
sessionId,
sessionName: workspace.sessionName ?? events.metadata.sessionName ?? null,
workspaceDirectory: workspace.workspaceDirectory ?? events.metadata.workspaceDirectory ?? null,
repository: workspace.repository ?? null,
branch: workspace.branch ?? null,
source: sessionId === options.currentSessionId ? "active-session-events" : "completed-session-events",
sourcePath: eventsPath,
status: sessionId === options.currentSessionId ? "active" : "completed",
currentModel: event.data.currentModel ?? null,
metricsTimestamp: event.timestamp,
latestEventType: events.latestEvent?.type ?? null,
latestEventTimestamp: events.latestEvent?.timestamp ?? null,
metricsStale: events.latestEvent?.timestamp !== event.timestamp,
updatedAt: events.latestEvent?.timestamp ?? event.timestamp ?? new Date(fs.statSync(eventsPath).mtimeMs).toISOString(),
totalNanoAiu: readOptionalNumber2(event.data.totalNanoAiu),
totalUserRequests: readOptionalNumber2(event.data.totalUserRequests),
totalApiDurationMs: readOptionalNumber2(event.data.totalApiDurationMs),
sessionStartTime: event.data.sessionStartTime ?? null,
lastCallInputTokens: readOptionalNumber2(event.data.lastCallInputTokens),
lastCallOutputTokens: readOptionalNumber2(event.data.lastCallOutputTokens),
modelUsage
};
}
function readSessionEvents(eventsPath) {
let latestMetricsEvent = null;
let richestMetricsEvent = null;
let latestEvent = null;
const metadata = {};
for (const line of fs.readFileSync(eventsPath, "utf8").split(/\r?\n/)) {
if (!line.trim()) {
continue;
}
const event = JSON.parse(line);
latestEvent = event;
updateMetadata(metadata, event);
if (event?.data?.modelMetrics || event?.data?.totalNanoAiu !== void 0) {
latestMetricsEvent = event;
if (!richestMetricsEvent || eventWeight(event) > eventWeight(richestMetricsEvent)) {
richestMetricsEvent = event;
}
}
}
return { latestMetricsEvent, richestMetricsEvent, latestEvent, metadata };
}
function readSessionWorkspaceMetadata(sessionId, options = {}) {
const workspacePath = options.workspacePath ?? path.join(getSessionStateDirectory(options), sessionId, "workspace.yaml");
if (!fs.existsSync(workspacePath)) {
return {};
}
const result = {};
for (const line of fs.readFileSync(workspacePath, "utf8").split(/\r?\n/)) {
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
if (!match) {
continue;
}
result[match[1]] = unquoteYamlScalar(match[2]);
}
return {
sessionName: readString(result.name),
workspaceDirectory: readString(result.cwd),
repository: readString(result.repository),
branch: readString(result.branch)
};
}
function updateMetadata(metadata, event) {
const data = event.data ?? {};
metadata.sessionName = readString(data.sessionName ?? data.session_name ?? data.name ?? metadata.sessionName) ?? metadata.sessionName;
metadata.workspaceDirectory = readString(data.workspaceDirectory ?? data.workspace_directory ?? data.cwd ?? metadata.workspaceDirectory) ?? metadata.workspaceDirectory;
}
function eventWeight(event) {
let total = numberOrZero2(event?.data?.totalNanoAiu);
for (const metrics of Object.values(event?.data?.modelMetrics ?? {})) {
total += numberOrZero2(metrics?.totalNanoAiu) + numberOrZero2(metrics?.usage?.inputTokens) + numberOrZero2(metrics?.usage?.cacheReadTokens) + numberOrZero2(metrics?.usage?.cacheWriteTokens) + numberOrZero2(metrics?.usage?.outputTokens) + numberOrZero2(metrics?.usage?.reasoningTokens);
}
return total;
}
function readString(value) {
if (value === void 0 || value === null || value === "") {
return void 0;
}
return String(value);
}
function unquoteYamlScalar(value) {
const trimmed = String(value ?? "").trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function readOptionalNumber2(value) {
if (value === void 0 || value === null || value === "") {
return void 0;
}
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
}
function numberOrZero2(value) {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
}
// .github/extensions/copilot-app-cost/lib/settings.mjs
import fs2 from "node:fs";
import os2 from "node:os";
import path2 from "node:path";
var APP_NAME = "copilot-app-cost";
var SETTINGS_FILE = "settings.json";
function getAppDataDirectory(options = {}) {
const env = options.env ?? process.env;
const homeDirectory = options.homeDirectory ?? os2.homedir();
if (process.platform === "win32") {
return path2.join(env.LOCALAPPDATA ?? path2.join(homeDirectory, "AppData", "Local"), APP_NAME);
}
if (process.platform === "darwin") {
return path2.join(homeDirectory, "Library", "Application Support", APP_NAME);
}
return path2.join(env.XDG_STATE_HOME ?? path2.join(homeDirectory, ".local", "state"), APP_NAME);
}
function getSettingsPath(options = {}) {
return path2.join(getAppDataDirectory(options), SETTINGS_FILE);
}
function getDefaultSettings(now = /* @__PURE__ */ new Date()) {
return {
billingScope: "user",
userAccount: "",
organizationAccount: "",
billingYear: now.getUTCFullYear(),
billingMonth: now.getUTCMonth() + 1,
liveRefreshSeconds: 2,
sessionHistoryRefreshSeconds: 30,
billingRefreshMinutes: 5,
alerts: {
sessionAiCredits: null,
sessionUsd: null,
monthlyUsagePercent: null,
monthlyNetUsd: null
},
diagnosticsEnabled: true,
billReasoningTokens: false,
updatedAt: now.toISOString()
};
}
function loadSettings(options = {}) {
const settingsPath = getSettingsPath(options);
const defaults = getDefaultSettings(options.now ? new Date(options.now) : /* @__PURE__ */ new Date());
if (!fs2.existsSync(settingsPath)) {
return defaults;
}
try {
const parsed = JSON.parse(fs2.readFileSync(settingsPath, "utf8"));
return normalizeSettings(parsed, defaults);
} catch {
return defaults;
}
}
function saveSettings(input, options = {}) {
const settingsPath = getSettingsPath(options);
const normalized = normalizeSettings(input, getDefaultSettings(options.now ? new Date(options.now) : /* @__PURE__ */ new Date()));
fs2.mkdirSync(path2.dirname(settingsPath), { recursive: true });
fs2.writeFileSync(settingsPath, `${JSON.stringify(normalized, null, 2)}
`);
return normalized;
}
function normalizeSettings(input, defaults = getDefaultSettings()) {
const billingScope = ["user", "organization"].includes(String(input?.billingScope ?? "")) ? String(input.billingScope) : defaults.billingScope;
return {
billingScope,
userAccount: sanitizeAccount(input?.userAccount),
organizationAccount: sanitizeAccount(input?.organizationAccount),
billingYear: clampInteger(input?.billingYear, 2024, 2100, defaults.billingYear),
billingMonth: clampInteger(input?.billingMonth, 1, 12, defaults.billingMonth),
liveRefreshSeconds: clampInteger(input?.liveRefreshSeconds, 2, 10, defaults.liveRefreshSeconds),
sessionHistoryRefreshSeconds: clampInteger(input?.sessionHistoryRefreshSeconds, 10, 300, defaults.sessionHistoryRefreshSeconds),
billingRefreshMinutes: clampInteger(input?.billingRefreshMinutes, 1, 60, defaults.billingRefreshMinutes),
alerts: {
sessionAiCredits: clampNullableNumber(input?.alerts?.sessionAiCredits, 0, 1e5),
sessionUsd: clampNullableNumber(input?.alerts?.sessionUsd, 0, 1e5),
monthlyUsagePercent: clampNullableNumber(input?.alerts?.monthlyUsagePercent, 0, 1e4),
monthlyNetUsd: clampNullableNumber(input?.alerts?.monthlyNetUsd, 0, 1e5)
},
diagnosticsEnabled: input?.diagnosticsEnabled !== false,
billReasoningTokens: input?.billReasoningTokens === true,
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
};
}
function sanitizeAccount(value) {
const trimmed = String(value ?? "").trim();
return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(trimmed) ? trimmed : "";
}
function clampInteger(value, min, max, fallback) {
const parsed = Number(value);
if (!Number.isInteger(parsed)) {
return fallback;
}
return Math.min(Math.max(parsed, min), max);
}
function clampNullableNumber(value, min, max) {
if (value === void 0 || value === null || value === "") {
return null;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return null;
}
return Math.min(Math.max(parsed, min), max);
}
// .github/extensions/copilot-app-cost/lib/billing.mjs
var BILLING_API_VERSION = "2026-03-10";
function validateAccountName(value) {
return typeof value === "string" && /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(value);
}
function buildGhArgs(request) {
if (!validateAccountName(request?.account)) {
throw new Error(`Invalid ${request?.scope ?? "billing"} account.`);
}
const basePath = request.scope === "organization" ? `/organizations/${request.account}/settings/billing/ai_credit/usage` : `/users/${request.account}/settings/billing/ai_credit/usage`;
const search = new URLSearchParams();
if (Number.isInteger(request.year)) {
search.set("year", String(request.year));
}
if (Number.isInteger(request.month)) {
search.set("month", String(request.month));
}
const pathWithQuery = search.size > 0 ? `${basePath}?${search.toString()}` : basePath;
return [
"api",
pathWithQuery,
"-H",
"Accept: application/vnd.github+json",
"-H",
`X-GitHub-Api-Version: ${request.apiVersion ?? BILLING_API_VERSION}`
];
}
function normalizeBillingResponse(request, payload, options = {}) {
const usageItems = Array.isArray(payload?.usageItems) ? payload.usageItems : [];
const totals = usageItems.reduce((acc, item) => ({
grossQuantity: acc.grossQuantity + numberOrZero3(item?.grossQuantity),
grossAmount: acc.grossAmount + numberOrZero3(item?.grossAmount),
discountQuantity: acc.discountQuantity + numberOrZero3(item?.discountQuantity),
discountAmount: acc.discountAmount + numberOrZero3(item?.discountAmount),
netQuantity: acc.netQuantity + numberOrZero3(item?.netQuantity),
netAmount: acc.netAmount + numberOrZero3(item?.netAmount)
}), {
grossQuantity: 0,
grossAmount: 0,
discountQuantity: 0,
discountAmount: 0,
netQuantity: 0,
netAmount: 0
});
const usageByModelMap = /* @__PURE__ */ new Map();
for (const item of usageItems) {
const model = readString2(item?.model) ?? "(unspecified)";
const target = usageByModelMap.get(model) ?? {
model,
product: readString2(item?.product) ?? null,
sku: readString2(item?.sku) ?? null,
grossQuantity: 0,
grossAmount: 0,
discountQuantity: 0,
discountAmount: 0,
netQuantity: 0,
netAmount: 0,
unitType: readString2(item?.unitType) ?? null,
pricePerUnit: item?.pricePerUnit ?? null
};
target.grossQuantity += numberOrZero3(item?.grossQuantity);
target.grossAmount += numberOrZero3(item?.grossAmount);
target.discountQuantity += numberOrZero3(item?.discountQuantity);
target.discountAmount += numberOrZero3(item?.discountAmount);
target.netQuantity += numberOrZero3(item?.netQuantity);
target.netAmount += numberOrZero3(item?.netAmount);
usageByModelMap.set(model, target);
}
const usageByModel = Array.from(usageByModelMap.values()).sort((a, b) => b.netAmount - a.netAmount || b.netQuantity - a.netQuantity);
const reliability = request.scope === "organization" ? "authoritative-scope" : totals.netQuantity > 0 ? "authoritative-scope" : "managed-license-or-no-personal-usage";
return {
source: "GITHUB BILLING",
scope: request.scope,
account: request.account,
freshness: "point-in-time",
confidence: "official",
retrievedAt: options.retrievedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
apiVersion: request.apiVersion ?? BILLING_API_VERSION,
timePeriod: payload?.timePeriod ?? { year: request.year, month: request.month },
product: payload?.product ?? null,
model: payload?.model ?? null,
user: payload?.user ?? null,
organization: payload?.organization ?? null,
totals,
usageByModel,
usageItemCount: usageItems.length,
permissionStatus: "authorized",
availability: true,
reliability,
managedLicenseNote: request.scope === "user" && totals.netQuantity <= 0 ? "Personal billing may be empty when Copilot usage is managed and billed by an organization or enterprise." : null
};
}
function classifyBillingError(message, scope) {
const text = String(message ?? "");
if (/needs the "user" scope/i.test(text)) {
return "missing-user-scope";
}
if (/HTTP 403|Forbidden/i.test(text)) {
return scope === "organization" ? "missing-organization-permission" : "forbidden";
}
if (/HTTP 401|Unauthorized/i.test(text)) {
return "unauthorized";
}
if (/HTTP 404|Not Found/i.test(text)) {
return scope === "organization" ? "not-found-or-no-org-access" : "not-found-or-no-billed-usage";
}
if (/HTTP 429|rate limit/i.test(text)) {
return "rate-limited";
}
if (/HTTP 5\d\d/i.test(text)) {
return "server-error";
}
return "request-failed";
}
function readString2(value) {
if (value === void 0 || value === null || value === "") {
return void 0;
}
return String(value);
}
function numberOrZero3(value) {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
}
// .github/extensions/copilot-app-cost/extension.mjs
var execFileAsync = promisify(execFile);
var EXTENSION_VERSION = "0.1.0";
var GH_TIMEOUT_MS = 1e4;
var canvasState = {
// Live session metrics from Copilot SDK
liveEstimate: null,
liveTimeline: [],
// GitHub official billing
billingData: null,
billingScope: "user",
billingAccount: "",
// Local session history
sessions: [],
// User settings (persist locally)
settings: loadSettings(),
// UI state
ui: {
activeTab: "overview",
expandedSessions: [],
showDiagnostics: false
},
// Provider status
providers: {
live: { status: "idle", error: null, lastRefresh: null },
billing: { status: "idle", error: null, lastRefresh: null },
sessions: { status: "idle", error: null, lastRefresh: null }
},
// Version info
extensionVersion: EXTENSION_VERSION
};
async function refreshLiveMetrics(session) {
canvasState.providers.live.status = "loading";
try {
const metrics = await session.rpc.usage.getMetrics?.();
if (!metrics) {
canvasState.liveEstimate = null;
canvasState.providers.live.status = "unavailable";
canvasState.providers.live.error = "No active session metrics available";
return { success: false, error: "No active session metrics" };
}
const usage = metricsToSessionUsage(metrics);
const estimate = calculateSessionEstimate(usage);
canvasState.liveEstimate = { usage, estimate, rawMetrics: metrics };
canvasState.providers.live.status = "success";
canvasState.providers.live.error = null;
canvasState.providers.live.lastRefresh = (/* @__PURE__ */ new Date()).toISOString();
return { success: true, estimate };
} catch (error) {
canvasState.providers.live.status = "error";
canvasState.providers.live.error = error?.message || String(error);
return { success: false, error: error?.message };
}
}
async function refreshBillingUsage(session, scope, account) {
canvasState.providers.billing.status = "loading";
try {
if (!validateAccountName(account)) {
throw new Error("Invalid account name");
}
const endpoint = scope === "org" ? `/organizations/${account}/settings/billing/ai_credit/usage` : `/users/${account}/settings/billing/ai_credit/usage`;
const args = buildGhArgs("api", endpoint, "--paginate");
const { stdout } = await execFileAsync("gh", args, { timeout: GH_TIMEOUT_MS });
const response = JSON.parse(stdout);
const normalized = normalizeBillingResponse(response, scope);
canvasState.billingData = normalized;
canvasState.billingScope = scope;
canvasState.billingAccount = account;
canvasState.providers.billing.status = "success";
canvasState.providers.billing.error = null;
canvasState.providers.billing.lastRefresh = (/* @__PURE__ */ new Date()).toISOString();
return { success: true, billing: normalized };
} catch (error) {
const classification = classifyBillingError(error);
canvasState.providers.billing.status = "error";
canvasState.providers.billing.error = classification;
return { success: false, error: classification };
}
}
async function refreshSessionHistory() {
canvasState.providers.sessions.status = "loading";
try {
const sessions = await listRecentSessionUsages();
canvasState.sessions = sessions;
canvasState.providers.sessions.status = "success";
canvasState.providers.sessions.error = null;
canvasState.providers.sessions.lastRefresh = (/* @__PURE__ */ new Date()).toISOString();
return { success: true, sessions };
} catch (error) {
canvasState.providers.sessions.status = "error";
canvasState.providers.sessions.error = error?.message || String(error);
return { success: false, error: error?.message };
}
}
function updateSettings(newSettings) {
const updated = { ...canvasState.settings, ...newSettings };
saveSettings(updated);
canvasState.settings = updated;
return { success: true, settings: updated };
}
function updateUIState(uiChanges) {
canvasState.ui = { ...canvasState.ui, ...uiChanges };
return { success: true, ui: canvasState.ui };
}
function exportDashboardData() {
return {
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
extensionVersion: EXTENSION_VERSION,
live: canvasState.liveEstimate,
billing: canvasState.billingData,
sessions: canvasState.sessions,
settings: {
billingScope: canvasState.billingScope,
billingAccount: canvasState.billingAccount
}
};
}
function initialize(session) {
return joinSession({
canvases: [
createCanvas({
name: "Copilot App Cost",
description: "Monitor AI-credit usage from active session and official GitHub billing",
type: "dashboard",
state: canvasState,
capabilities: {
"refresh-live": () => refreshLiveMetrics(session),
"refresh-billing": (scope, account) => refreshBillingUsage(session, scope, account),
"refresh-sessions": () => refreshSessionHistory(),
"update-settings": (settings) => updateSettings(settings),
"update-ui": (ui) => updateUIState(ui),
"export-data": () => exportDashboardData()
}
})
]
});
}
export {
initialize as default
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment