mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
* chore(usage): extract crof, nanogpt, qoder, opencode, deepseek, bailian, vertex, xiaomi-mimo, xai, github usage fetchers into usage/* leaves Decompose services/usage.ts (god-file phase 1): move the remaining per-provider usage fetcher/parser logic into co-located leaves under open-sse/services/usage/ so usage.ts becomes a thin dispatcher (imports + USAGE_FETCHER_PROVIDERS + getUsageForProvider switch + __testing re-exports). New leaves (each a pure data transform or independent fetcher, no orchestration): - usage/github.ts getGitHubUsage, formatGitHubQuotaSnapshot, inferGitHubPlanName, shouldDisplayGitHubQuota - usage/crof.ts getCrofUsage - usage/nanogpt.ts getNanoGptUsage - usage/qoder.ts getQoderUsage, parseQoderUserStatusUsage - usage/opencode.ts getOpencodeUsage - usage/deepseek.ts getDeepseekUsage - usage/bailian.ts getBailianCodingPlanUsage - usage/vertex.ts getVertexUsage - usage/xiaomi-mimo.ts getXiaomiMimoUsage - usage/xai.ts getXaiUsage usage.ts re-exports parseQoderUserStatusUsage (named) and threads every helper the existing __testing contract exposes (usage-utils / usage-service-hardening / qoder-usage-quota / xiaomi-mimo-selftrack / xai-usage / vertex-spend suites read them from services/usage). External importers unchanged. open-sse/services/usage.ts: 1065 -> 256 lines (cap 800). npm run check:file-size: OK. typecheck:core: OK. eslint: clean. check:cycles: OK. Added characterization tests (tests/unit/usage-<provider>-split.test.ts) that import each leaf directly and pin its export surface + key edges, mirroring the existing usage-quota-core-split / usage-scalars-split pattern. * docs(changelog): add fragment for this PR
231 lines
8.9 KiB
TypeScript
231 lines
8.9 KiB
TypeScript
/**
|
|
* usage/github.ts — GitHub Copilot usage fetcher + quota/plan helpers.
|
|
*
|
|
* Extracted from services/usage.ts (god-file decomposition): the GitHub family —
|
|
* the copilot_internal/user fetcher (getGitHubUsage), the paid/limited quota
|
|
* snapshot formatter (formatGitHubQuotaSnapshot), the plan-name inference
|
|
* (inferGitHubPlanName), and the display gate (shouldDisplayGitHubQuota).
|
|
* Depends only on the sibling scalar/quota leaves + the GitHub internal-user
|
|
* header profile — no host coupling — so it lives as a co-located provider leaf.
|
|
* usage.ts imports getGitHubUsage (dispatcher) + re-exports the helpers via
|
|
* __testing (existing usage-utils / usage-service-hardening suites read them
|
|
* from there). Behavior-preserving move.
|
|
*/
|
|
|
|
import { getGitHubCopilotInternalUserHeaders } from "../../config/providerHeaderProfiles.ts";
|
|
import { toRecord, toNumber, getFieldValue, clampPercentage, toDisplayLabel } from "./scalars.ts";
|
|
import { type UsageQuota, parseResetTime } from "./quota.ts";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
export function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota {
|
|
if (!quota) return false;
|
|
if (quota.unlimited && quota.total <= 0) return false;
|
|
return quota.total > 0 || quota.remainingPercentage !== undefined;
|
|
}
|
|
|
|
/**
|
|
* GitHub Copilot Usage
|
|
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
|
|
*/
|
|
export async function getGitHubUsage(accessToken?: string, providerSpecificData?: JsonRecord) {
|
|
try {
|
|
if (!accessToken) {
|
|
throw new Error("No GitHub access token available. Please re-authorize the connection.");
|
|
}
|
|
|
|
// copilot_internal/user API requires GitHub OAuth token, not copilotToken
|
|
const response = await fetch("https://api.github.com/copilot_internal/user", {
|
|
headers: getGitHubCopilotInternalUserHeaders(`token ${accessToken}`),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (response.status === 401 || response.status === 403) {
|
|
return {
|
|
message: `GitHub token expired or permission denied. Please re-authenticate the connection.`,
|
|
};
|
|
}
|
|
throw new Error(`GitHub API error: ${error}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const dataRecord = toRecord(data);
|
|
|
|
// Handle different response formats (paid vs free)
|
|
if (dataRecord.quota_snapshots) {
|
|
// Paid plan format
|
|
const snapshots = toRecord(dataRecord.quota_snapshots);
|
|
const resetAt = parseResetTime(
|
|
getFieldValue(dataRecord, "quota_reset_date", "quotaResetDate")
|
|
);
|
|
const premiumQuota = formatGitHubQuotaSnapshot(snapshots.premium_interactions, resetAt);
|
|
const chatQuota = formatGitHubQuotaSnapshot(snapshots.chat, resetAt);
|
|
const completionsQuota = formatGitHubQuotaSnapshot(snapshots.completions, resetAt);
|
|
const quotas: Record<string, UsageQuota> = {};
|
|
|
|
if (shouldDisplayGitHubQuota(premiumQuota)) {
|
|
quotas.premium_interactions = premiumQuota;
|
|
}
|
|
if (shouldDisplayGitHubQuota(chatQuota)) {
|
|
quotas.chat = chatQuota;
|
|
}
|
|
if (shouldDisplayGitHubQuota(completionsQuota)) {
|
|
quotas.completions = completionsQuota;
|
|
}
|
|
|
|
return {
|
|
plan: inferGitHubPlanName(dataRecord, premiumQuota),
|
|
resetDate: getFieldValue(dataRecord, "quota_reset_date", "quotaResetDate"),
|
|
quotas,
|
|
};
|
|
} else if (dataRecord.monthly_quotas || dataRecord.limited_user_quotas) {
|
|
// Free/limited plan format. NOTE (#2876): the upstream field
|
|
// `limited_user_quotas[name]` is the *remaining* count for the month
|
|
// (it counts down toward 0 and resets on `limited_user_reset_date`),
|
|
// NOT the used count. The pre-3.8.6 implementation inverted this and
|
|
// showed "0% when not used / 100% when fully used" on the dashboard.
|
|
// Confirmed against three independent upstream parsers:
|
|
// - robinebers/openusage docs/providers/copilot.md (Free Tier table)
|
|
// - raycast/extensions agent-usage/src/copilot/fetcher.ts (inline comment)
|
|
// - looplj/axonhub frontend/src/components/quota-badges.tsx
|
|
const monthlyQuotas = toRecord(dataRecord.monthly_quotas);
|
|
const remainingQuotas = toRecord(dataRecord.limited_user_quotas);
|
|
const resetDate = getFieldValue(
|
|
dataRecord,
|
|
"limited_user_reset_date",
|
|
"limitedUserResetDate"
|
|
);
|
|
const resetAt = parseResetTime(resetDate);
|
|
const quotas: Record<string, UsageQuota> = {};
|
|
|
|
const addLimitedQuota = (name: string) => {
|
|
const total = toNumber(getFieldValue(monthlyQuotas, name, name), 0);
|
|
if (total <= 0) return null;
|
|
const remainingRaw = Math.max(0, toNumber(getFieldValue(remainingQuotas, name, name), 0));
|
|
const remaining = Math.min(remainingRaw, total);
|
|
const used = Math.max(total - remaining, 0);
|
|
quotas[name] = {
|
|
used,
|
|
total,
|
|
remaining,
|
|
remainingPercentage: clampPercentage((remaining / total) * 100),
|
|
unlimited: false,
|
|
resetAt,
|
|
};
|
|
return quotas[name];
|
|
};
|
|
|
|
const premiumQuota = addLimitedQuota("premium_interactions");
|
|
addLimitedQuota("chat");
|
|
addLimitedQuota("completions");
|
|
|
|
return {
|
|
plan: inferGitHubPlanName(dataRecord, premiumQuota),
|
|
resetDate,
|
|
quotas,
|
|
};
|
|
}
|
|
|
|
return { message: "GitHub Copilot connected. Unable to parse quota data." };
|
|
} catch (error) {
|
|
throw new Error(`Failed to fetch GitHub usage: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
export function formatGitHubQuotaSnapshot(
|
|
quota: unknown,
|
|
resetAt: string | null = null
|
|
): UsageQuota | null {
|
|
const source = toRecord(quota);
|
|
if (Object.keys(source).length === 0) return null;
|
|
|
|
const unlimited = source.unlimited === true;
|
|
const entitlement = toNumber(source.entitlement, Number.NaN);
|
|
const totalValue = toNumber(source.total, Number.NaN);
|
|
const remainingValue = toNumber(source.remaining, Number.NaN);
|
|
const usedValue = toNumber(source.used, Number.NaN);
|
|
const percentRemainingValue = toNumber(
|
|
getFieldValue(source, "percent_remaining", "percentRemaining"),
|
|
Number.NaN
|
|
);
|
|
|
|
let total = Number.isFinite(totalValue)
|
|
? Math.max(0, totalValue)
|
|
: Number.isFinite(entitlement)
|
|
? Math.max(0, entitlement)
|
|
: 0;
|
|
let remaining = Number.isFinite(remainingValue) ? Math.max(0, remainingValue) : undefined;
|
|
let used = Number.isFinite(usedValue) ? Math.max(0, usedValue) : undefined;
|
|
let remainingPercentage = Number.isFinite(percentRemainingValue)
|
|
? clampPercentage(percentRemainingValue)
|
|
: undefined;
|
|
|
|
if (used === undefined && total > 0 && remaining !== undefined) {
|
|
used = Math.max(total - remaining, 0);
|
|
}
|
|
|
|
if (remaining === undefined && total > 0 && used !== undefined) {
|
|
remaining = Math.max(total - used, 0);
|
|
}
|
|
|
|
if (remainingPercentage === undefined && total > 0 && remaining !== undefined) {
|
|
remainingPercentage = clampPercentage((remaining / total) * 100);
|
|
}
|
|
|
|
if (total <= 0 && remainingPercentage !== undefined) {
|
|
total = 100;
|
|
used = 100 - remainingPercentage;
|
|
remaining = remainingPercentage;
|
|
}
|
|
|
|
return {
|
|
used: Math.max(0, used ?? 0),
|
|
total,
|
|
remaining,
|
|
remainingPercentage,
|
|
resetAt,
|
|
unlimited,
|
|
};
|
|
}
|
|
|
|
export function inferGitHubPlanName(data: JsonRecord, premiumQuota: UsageQuota | null): string {
|
|
const rawPlan = getFieldValue(data, "copilot_plan", "copilotPlan");
|
|
const rawSku = getFieldValue(data, "access_type_sku", "accessTypeSku");
|
|
const planText = typeof rawPlan === "string" ? rawPlan.trim() : "";
|
|
const skuText = typeof rawSku === "string" ? rawSku.trim() : "";
|
|
const combined = `${skuText} ${planText}`.trim().toUpperCase();
|
|
const monthlyQuotas = toRecord(getFieldValue(data, "monthly_quotas", "monthlyQuotas"));
|
|
const premiumTotal =
|
|
premiumQuota?.total ||
|
|
toNumber(getFieldValue(monthlyQuotas, "premium_interactions", "premiumInteractions"), 0);
|
|
const chatTotal = toNumber(getFieldValue(monthlyQuotas, "chat", "chat"), 0);
|
|
|
|
if (combined.includes("PRO+") || combined.includes("PRO_PLUS") || combined.includes("PROPLUS")) {
|
|
return "Copilot Pro+";
|
|
}
|
|
if (combined.includes("ENTERPRISE")) return "Copilot Enterprise";
|
|
if (combined.includes("BUSINESS")) return "Copilot Business";
|
|
if (combined.includes("STUDENT")) return "Copilot Student";
|
|
if (combined.includes("FREE")) return "Copilot Free";
|
|
if (combined.includes("PRO")) return "Copilot Pro";
|
|
|
|
if (premiumTotal >= 1400) return "Copilot Pro+";
|
|
if (premiumTotal >= 900) return "Copilot Enterprise";
|
|
if (premiumTotal >= 250) {
|
|
if (combined.includes("INDIVIDUAL")) return "Copilot Pro";
|
|
return "Copilot Business";
|
|
}
|
|
if (premiumTotal > 0 || chatTotal === 50) return "Copilot Free";
|
|
|
|
if (skuText) {
|
|
const label = toDisplayLabel(skuText);
|
|
return label ? `Copilot ${label}` : "GitHub Copilot";
|
|
}
|
|
if (planText) {
|
|
const label = toDisplayLabel(planText);
|
|
return label ? `Copilot ${label}` : "GitHub Copilot";
|
|
}
|
|
return "GitHub Copilot";
|
|
}
|