@openhoo/hoopilot 2.1.5 → 2.1.6

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/dist/cli.js CHANGED
@@ -2613,10 +2613,10 @@ footer.foot .end { margin-left:auto; }
2613
2613
  pollGen += 1; var myGen = pollGen;
2614
2614
  if (inflightFetch){ try { inflightFetch.abort(); } catch (e) {} }
2615
2615
  var ctrl = new AbortController(); inflightFetch = ctrl;
2616
- var to = setTimeout(function(){ try { ctrl.abort(); } catch (e) {} }, 3000);
2616
+ var to = setTimeout(function(){ try { ctrl.abort(); } catch (e) {} }, Math.max(10000, intervalMs * 2));
2617
2617
  var headers = { "accept":"application/json" };
2618
2618
  if (apiKey) headers["x-api-key"] = apiKey;
2619
- fetch("/v1/usage", { headers: headers, signal: ctrl.signal, cache:"no-store" }).then(function(res){
2619
+ fetch("/v1/usage?view=dashboard", { headers: headers, signal: ctrl.signal, cache:"no-store" }).then(function(res){
2620
2620
  clearTimeout(to);
2621
2621
  if (myGen !== pollGen) return null;
2622
2622
  if (res.status === 401 || res.status === 403){ inflightFetch = null; showAuth(!!apiKey); return null; }
@@ -2898,6 +2898,7 @@ var MetricsRegistry = class {
2898
2898
  #inFlight = 0;
2899
2899
  #requests = /* @__PURE__ */ new Map();
2900
2900
  #durations = /* @__PURE__ */ new Map();
2901
+ #inFlightByRoute = /* @__PURE__ */ new Map();
2901
2902
  #tokens = /* @__PURE__ */ new Map();
2902
2903
  #upstream = /* @__PURE__ */ new Map();
2903
2904
  #copilotQuota;
@@ -2907,14 +2908,23 @@ var MetricsRegistry = class {
2907
2908
  this.#startedAtMs = (options.now ?? Date.now)();
2908
2909
  }
2909
2910
  /** Mark a request as started; pair with exactly one {@link observe}. */
2910
- startRequest() {
2911
+ startRequest(route) {
2911
2912
  this.#inFlight += 1;
2913
+ if (route) {
2914
+ this.#inFlightByRoute.set(route, (this.#inFlightByRoute.get(route) ?? 0) + 1);
2915
+ }
2912
2916
  }
2913
2917
  /** Record a completed request and clear its in-flight slot. */
2914
2918
  observe(observation) {
2915
2919
  if (this.#inFlight > 0) {
2916
2920
  this.#inFlight -= 1;
2917
2921
  }
2922
+ const inFlightForRoute = this.#inFlightByRoute.get(observation.route) ?? 0;
2923
+ if (inFlightForRoute > 1) {
2924
+ this.#inFlightByRoute.set(observation.route, inFlightForRoute - 1);
2925
+ } else if (inFlightForRoute === 1) {
2926
+ this.#inFlightByRoute.delete(observation.route);
2927
+ }
2918
2928
  const key = labelKey(observation.route, observation.method, String(observation.status));
2919
2929
  this.#requests.set(key, (this.#requests.get(key) ?? 0) + 1);
2920
2930
  this.#observeDuration(observation.route, observation.durationMs / 1e3);
@@ -2999,12 +3009,19 @@ var MetricsRegistry = class {
2999
3009
  this.#durations.set(route, entry);
3000
3010
  }
3001
3011
  /** A JSON-friendly view of the current counters. */
3002
- snapshot(now = Date.now) {
3012
+ snapshot(nowOrOptions = Date.now) {
3013
+ const options = typeof nowOrOptions === "function" ? { now: nowOrOptions } : nowOrOptions;
3014
+ const now = options.now ?? Date.now;
3015
+ const excludeRoutes = new Set(options.excludeRoutes ?? []);
3016
+ const excludeUpstreamPaths = new Set(options.excludeUpstreamPaths ?? []);
3003
3017
  const byRoute = {};
3004
3018
  const byStatus = {};
3005
3019
  let requestsTotal = 0;
3006
3020
  for (const [key, count] of this.#requests) {
3007
3021
  const [route = "", , status = ""] = key.split(LABEL_SEPARATOR);
3022
+ if (excludeRoutes.has(route)) {
3023
+ continue;
3024
+ }
3008
3025
  byRoute[route] = (byRoute[route] ?? 0) + count;
3009
3026
  byStatus[status] = (byStatus[status] ?? 0) + count;
3010
3027
  requestsTotal += count;
@@ -3022,8 +3039,12 @@ var MetricsRegistry = class {
3022
3039
  let upstreamTotal = 0;
3023
3040
  let upstreamErrors = 0;
3024
3041
  for (const [key, count] of this.#upstream) {
3042
+ const [path = "", outcome = ""] = key.split(LABEL_SEPARATOR);
3043
+ if (excludeUpstreamPaths.has(path)) {
3044
+ continue;
3045
+ }
3025
3046
  upstreamTotal += count;
3026
- if (key.endsWith(`${LABEL_SEPARATOR}error`)) {
3047
+ if (outcome === "error") {
3027
3048
  upstreamErrors += count;
3028
3049
  }
3029
3050
  }
@@ -3033,8 +3054,8 @@ var MetricsRegistry = class {
3033
3054
  }
3034
3055
  return {
3035
3056
  githubRateLimit,
3036
- inFlight: this.#inFlight,
3037
- latency: this.#latencySnapshot(),
3057
+ inFlight: this.#filteredInFlight(excludeRoutes),
3058
+ latency: this.#latencySnapshot(excludeRoutes),
3038
3059
  requests: { byRoute, byStatus, total: requestsTotal },
3039
3060
  startedAt: new Date(this.#startedAtMs).toISOString(),
3040
3061
  tokens: { byModel, extraction: { ...this.#extraction }, ...tokenTotals },
@@ -3042,15 +3063,30 @@ var MetricsRegistry = class {
3042
3063
  uptimeSeconds: Math.max(0, Math.round((now() - this.#startedAtMs) / 1e3))
3043
3064
  };
3044
3065
  }
3066
+ #filteredInFlight(excludeRoutes) {
3067
+ if (excludeRoutes.size === 0) {
3068
+ return this.#inFlight;
3069
+ }
3070
+ let excluded = 0;
3071
+ for (const [route, count] of this.#inFlightByRoute) {
3072
+ if (excludeRoutes.has(route)) {
3073
+ excluded += count;
3074
+ }
3075
+ }
3076
+ return Math.max(0, this.#inFlight - excluded);
3077
+ }
3045
3078
  // Summarize the duration histogram into a JSON latency view: per-route count and
3046
3079
  // exact average, plus overall average and estimated p50/p95. The percentiles come
3047
3080
  // from the buckets aggregated across routes, so they share /metrics' resolution.
3048
- #latencySnapshot() {
3081
+ #latencySnapshot(excludeRoutes = /* @__PURE__ */ new Set()) {
3049
3082
  const byRoute = {};
3050
3083
  const aggregateBuckets = new Array(DURATION_BUCKETS_SECONDS.length).fill(0);
3051
3084
  let totalCount = 0;
3052
3085
  let totalSum = 0;
3053
3086
  for (const [route, entry] of this.#durations) {
3087
+ if (excludeRoutes.has(route)) {
3088
+ continue;
3089
+ }
3054
3090
  byRoute[route] = {
3055
3091
  avgMs: entry.count > 0 ? round2(entry.sum / entry.count * 1e3) : 0,
3056
3092
  count: entry.count
@@ -3583,6 +3619,15 @@ var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
3583
3619
  var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
3584
3620
  var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
3585
3621
  var USAGE_CACHE_TTL_MS = 6e4;
3622
+ var DASHBOARD_USAGE_VIEW = "dashboard";
3623
+ var DASHBOARD_EXCLUDED_ROUTES = [
3624
+ "cors.preflight",
3625
+ "dashboard",
3626
+ "health",
3627
+ "metrics",
3628
+ "usage"
3629
+ ];
3630
+ var DASHBOARD_EXCLUDED_UPSTREAM_PATHS = ["/copilot_internal/user"];
3586
3631
  var RequestBodyTooLargeError = class extends Error {
3587
3632
  constructor() {
3588
3633
  super(REQUEST_TOO_LARGE_MESSAGE);
@@ -3635,7 +3680,7 @@ function createHoopilotHandler(options = {}) {
3635
3680
  requestId,
3636
3681
  route
3637
3682
  });
3638
- metrics.startRequest();
3683
+ metrics.startRequest(route);
3639
3684
  const origin = request.headers.get("origin")?.trim() || void 0;
3640
3685
  const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
3641
3686
  const inner = normalizeInnerRequest(request, apiPath, url);
@@ -3758,7 +3803,7 @@ function buildApp(deps) {
3758
3803
  }
3759
3804
  logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
3760
3805
  return jsonError(500, "internal_error", message);
3761
- }).get("/", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/healthz", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/metrics", () => metricsResponse(metrics)).get("/v1/usage", ({ request }) => handleUsage(metrics, readUsage, request.signal)).get(
3806
+ }).get("/", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/healthz", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/metrics", () => metricsResponse(metrics)).get("/v1/usage", ({ request }) => handleUsage(metrics, readUsage, request)).get(
3762
3807
  "/v1/models",
3763
3808
  ({ request }) => handleModels(client, metrics, request.signal, loggerFor(request))
3764
3809
  ).get("/v1/responses", () => websocketUnsupportedResponse()).post(
@@ -4516,9 +4561,13 @@ function dashboardResponse() {
4516
4561
  status: 200
4517
4562
  });
4518
4563
  }
4519
- async function handleUsage(metrics, readUsage, signal) {
4520
- const { copilot, error } = await readUsage(signal);
4521
- const proxy = metrics.snapshot();
4564
+ async function handleUsage(metrics, readUsage, request) {
4565
+ const view = new URL(request.url).searchParams.get("view");
4566
+ const { copilot, error } = await readUsage(request.signal);
4567
+ const proxy = view === DASHBOARD_USAGE_VIEW ? metrics.snapshot({
4568
+ excludeRoutes: DASHBOARD_EXCLUDED_ROUTES,
4569
+ excludeUpstreamPaths: DASHBOARD_EXCLUDED_UPSTREAM_PATHS
4570
+ }) : metrics.snapshot();
4522
4571
  const body = {
4523
4572
  copilot: copilot ?? null,
4524
4573
  object: "usage",
@@ -4535,25 +4584,30 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
4535
4584
  let cache;
4536
4585
  return async (signal) => {
4537
4586
  if (cache && now() - cache.atMs < ttlMs) {
4538
- return { copilot: cache.value };
4587
+ return cache.result;
4539
4588
  }
4540
4589
  try {
4541
4590
  const upstream = await client.usage(signal);
4542
4591
  metrics.recordUpstream(usagePath, upstream.ok);
4543
4592
  metrics.recordGithubRateLimit(parseRateLimitHeaders(upstream.headers, now()));
4544
4593
  if (!upstream.ok) {
4545
- return { error: `GitHub Copilot usage request failed with ${upstream.status}.` };
4594
+ const result2 = { error: `GitHub Copilot usage request failed with ${upstream.status}.` };
4595
+ cache = { atMs: now(), result: result2 };
4596
+ return result2;
4546
4597
  }
4547
4598
  const value = normalizeCopilotUsage(await upstream.json().catch(() => ({})));
4548
- cache = { atMs: now(), value };
4599
+ const result = { copilot: value };
4600
+ cache = { atMs: now(), result };
4549
4601
  metrics.recordCopilotQuota(value);
4550
- return { copilot: value };
4602
+ return result;
4551
4603
  } catch (error) {
4552
4604
  if (error instanceof CopilotAuthError) {
4553
4605
  return { error: error.message };
4554
4606
  }
4555
4607
  metrics.recordUpstream(usagePath, false);
4556
- return { error: errorMessage(error) };
4608
+ const result = { error: errorMessage(error) };
4609
+ cache = { atMs: now(), result };
4610
+ return result;
4557
4611
  }
4558
4612
  };
4559
4613
  }