@openhoo/hoopilot 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -158,7 +158,7 @@ Incoming `x-request-id` headers are preserved on responses. If a request has no
158
158
 
159
159
  Hoopilot tracks token usage, request counts, and latency in memory while the server runs, and can report your GitHub Copilot account quota (premium-request "credit" usage).
160
160
 
161
- - `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4`). It exposes request counters (`hoopilot_requests_total`), upstream call counters (`hoopilot_upstream_requests_total`), token counters by model and type (`hoopilot_tokens_total{model,type}`), a request-duration histogram (`hoopilot_request_duration_seconds`), an in-flight gauge, and—once `/v1/usage` has been fetched at least once—Copilot quota gauges (`hoopilot_copilot_quota_remaining{category}`, `_entitlement`, `_used`, `_percent_remaining`). Counters reset to zero on restart, which Prometheus handles natively.
161
+ - `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4`). It exposes request counters (`hoopilot_requests_total`), upstream call counters (`hoopilot_upstream_requests_total`), token counters by model and type (`hoopilot_tokens_total{model,type}`), a request-duration histogram (`hoopilot_request_duration_seconds`), an in-flight gauge, and—once `/v1/usage` has been fetched at least once—Copilot quota gauges (`hoopilot_copilot_quota_remaining{category}`, `_entitlement`, `_used`, `_percent_remaining`, `_overage_count`, `_overage_entitlement`, `_unlimited`, `_overage_permitted`, `_has_quota`, `_token_based_billing`, and category reset/snapshot timestamps). Counters reset to zero on restart, which Prometheus handles natively.
162
162
  - `GET /v1/usage` returns JSON combining the proxy metrics snapshot with live Copilot quota fetched from GitHub (cached for 60 seconds). If the quota cannot be read, `copilot` is `null` and `copilot_error` explains why, but the proxy metrics are still returned.
163
163
  - `hoopilot usage` prints your Copilot plan and quota from the command line.
164
164
 
package/dist/cli.js CHANGED
@@ -293,22 +293,31 @@ function normalizeCopilotUsage(body) {
293
293
  }
294
294
  function normalizeQuotaDetail(detail) {
295
295
  const entitlement = numberOrUndefined(detail.entitlement);
296
+ const overageCount = numberOrUndefined(detail.overage_count);
296
297
  const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
297
298
  return removeUndefinedQuota({
298
299
  entitlement,
299
- overageCount: numberOrUndefined(detail.overage_count),
300
+ hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
301
+ overageCount,
302
+ overageEntitlement: numberOrUndefined(detail.overage_entitlement),
300
303
  overagePermitted: typeof detail.overage_permitted === "boolean" ? detail.overage_permitted : void 0,
301
304
  percentRemaining: numberOrUndefined(detail.percent_remaining),
305
+ quotaId: stringOrUndefined(detail.quota_id),
306
+ quotaResetAt: stringOrUndefined(detail.quota_reset_at),
302
307
  remaining,
308
+ timestampUtc: stringOrUndefined(detail.timestamp_utc),
309
+ tokenBasedBilling: typeof detail.token_based_billing === "boolean" ? detail.token_based_billing : void 0,
303
310
  unlimited: typeof detail.unlimited === "boolean" ? detail.unlimited : void 0,
304
- used: usedFrom(entitlement, remaining)
311
+ used: usedFrom(entitlement, remaining, overageCount)
305
312
  });
306
313
  }
307
- function usedFrom(entitlement, remaining) {
314
+ function usedFrom(entitlement, remaining, overageCount) {
308
315
  if (entitlement === void 0 || remaining === void 0) {
309
316
  return void 0;
310
317
  }
311
- return Math.max(0, entitlement - remaining);
318
+ const base = entitlement - remaining;
319
+ const overage = remaining === 0 ? overageCount ?? 0 : 0;
320
+ return Math.max(0, base + overage);
312
321
  }
313
322
  function numberOrUndefined(value) {
314
323
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
@@ -1094,11 +1103,43 @@ var MetricsRegistry = class {
1094
1103
  gauge("remaining", "Remaining quota for the Copilot category.", (q) => q.remaining);
1095
1104
  gauge("entitlement", "Quota entitlement for the Copilot category.", (q) => q.entitlement);
1096
1105
  gauge("used", "Used quota (entitlement minus remaining) for the category.", (q) => q.used);
1106
+ gauge("overage_count", "Overage count for the Copilot category.", (q) => q.overageCount);
1107
+ gauge(
1108
+ "overage_entitlement",
1109
+ "Overage entitlement for the Copilot category.",
1110
+ (q) => q.overageEntitlement
1111
+ );
1097
1112
  gauge(
1098
1113
  "percent_remaining",
1099
1114
  "Percent of quota remaining for the Copilot category.",
1100
1115
  (q) => q.percentRemaining
1101
1116
  );
1117
+ booleanGauge(
1118
+ "unlimited",
1119
+ "Whether the Copilot quota category is unlimited.",
1120
+ (q) => q.unlimited
1121
+ );
1122
+ booleanGauge(
1123
+ "overage_permitted",
1124
+ "Whether overage is permitted for the Copilot category.",
1125
+ (q) => q.overagePermitted
1126
+ );
1127
+ booleanGauge("has_quota", "Whether the Copilot quota category has a quota.", (q) => q.hasQuota);
1128
+ booleanGauge(
1129
+ "token_based_billing",
1130
+ "Whether the Copilot quota category uses token-based billing.",
1131
+ (q) => q.tokenBasedBilling
1132
+ );
1133
+ dateGauge(
1134
+ "category_reset_timestamp_seconds",
1135
+ "Unix epoch of the Copilot category-specific quota reset.",
1136
+ (q) => q.quotaResetAt
1137
+ );
1138
+ dateGauge(
1139
+ "category_snapshot_timestamp_seconds",
1140
+ "Unix epoch of the Copilot category quota snapshot.",
1141
+ (q) => q.timestampUtc
1142
+ );
1102
1143
  const resetMs = usage.quotaResetDate ? Date.parse(usage.quotaResetDate) : Number.NaN;
1103
1144
  if (Number.isFinite(resetMs)) {
1104
1145
  lines.push(
@@ -1117,6 +1158,30 @@ var MetricsRegistry = class {
1117
1158
  })} 1`
1118
1159
  );
1119
1160
  }
1161
+ function booleanGauge(suffix, help, pick) {
1162
+ const present = categories.filter(([, quota]) => pick(quota) !== void 0);
1163
+ if (present.length === 0) {
1164
+ return;
1165
+ }
1166
+ lines.push(`# HELP hoopilot_copilot_quota_${suffix} ${help}`);
1167
+ lines.push(`# TYPE hoopilot_copilot_quota_${suffix} gauge`);
1168
+ for (const [category, quota] of present) {
1169
+ lines.push(
1170
+ `hoopilot_copilot_quota_${suffix}${labels({ category })} ${pick(quota) ? 1 : 0}`
1171
+ );
1172
+ }
1173
+ }
1174
+ function dateGauge(suffix, help, pick) {
1175
+ const present = categories.map(([category, quota]) => [category, Date.parse(pick(quota) ?? "")]).filter(([, timestamp]) => Number.isFinite(timestamp));
1176
+ if (present.length === 0) {
1177
+ return;
1178
+ }
1179
+ lines.push(`# HELP hoopilot_copilot_quota_${suffix} ${help}`);
1180
+ lines.push(`# TYPE hoopilot_copilot_quota_${suffix} gauge`);
1181
+ for (const [category, timestamp] of present) {
1182
+ lines.push(`hoopilot_copilot_quota_${suffix}${labels({ category })} ${timestamp / 1e3}`);
1183
+ }
1184
+ }
1120
1185
  }
1121
1186
  };
1122
1187
  function observeResponseUsage(response, fallbackModel, onUsage, signal) {
@@ -1814,8 +1879,8 @@ function metricsResponse(metrics) {
1814
1879
  });
1815
1880
  }
1816
1881
  async function handleUsage(metrics, readUsage, signal) {
1817
- const proxy = metrics.snapshot();
1818
1882
  const { copilot, error } = await readUsage(signal);
1883
+ const proxy = metrics.snapshot();
1819
1884
  const body = { copilot: copilot ?? null, object: "usage", proxy };
1820
1885
  if (error) {
1821
1886
  body.copilot_error = error;
@@ -1840,10 +1905,10 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
1840
1905
  metrics.recordCopilotQuota(value);
1841
1906
  return { copilot: value };
1842
1907
  } catch (error) {
1843
- metrics.recordUpstream(usagePath, false);
1844
1908
  if (error instanceof CopilotAuthError) {
1845
1909
  return { error: error.message };
1846
1910
  }
1911
+ metrics.recordUpstream(usagePath, false);
1847
1912
  return { error: errorMessage(error) };
1848
1913
  }
1849
1914
  };