@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 +72 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +72 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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) {} },
|
|
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(
|
|
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 (
|
|
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.#
|
|
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
|
|
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,
|
|
4520
|
-
const
|
|
4521
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4599
|
+
const result = { copilot: value };
|
|
4600
|
+
cache = { atMs: now(), result };
|
|
4549
4601
|
metrics.recordCopilotQuota(value);
|
|
4550
|
-
return
|
|
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
|
-
|
|
4608
|
+
const result = { error: errorMessage(error) };
|
|
4609
|
+
cache = { atMs: now(), result };
|
|
4610
|
+
return result;
|
|
4557
4611
|
}
|
|
4558
4612
|
};
|
|
4559
4613
|
}
|