@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/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare class MetricsRegistry {
|
|
|
12
12
|
now?: () => number;
|
|
13
13
|
});
|
|
14
14
|
/** Mark a request as started; pair with exactly one {@link observe}. */
|
|
15
|
-
startRequest(): void;
|
|
15
|
+
startRequest(route?: string): void;
|
|
16
16
|
/** Record a completed request and clear its in-flight slot. */
|
|
17
17
|
observe(observation: RequestObservation): void;
|
|
18
18
|
/**
|
|
@@ -35,7 +35,7 @@ declare class MetricsRegistry {
|
|
|
35
35
|
*/
|
|
36
36
|
recordGithubRateLimit(rateLimit: GithubRateLimit | undefined): void;
|
|
37
37
|
/** A JSON-friendly view of the current counters. */
|
|
38
|
-
snapshot(
|
|
38
|
+
snapshot(nowOrOptions?: (() => number) | MetricsSnapshotOptions): MetricsSnapshot;
|
|
39
39
|
/** Render the Prometheus text exposition format (version 0.0.4). */
|
|
40
40
|
renderPrometheus(now?: () => number): string;
|
|
41
41
|
}
|
|
@@ -243,6 +243,12 @@ interface MetricsSnapshot {
|
|
|
243
243
|
};
|
|
244
244
|
uptimeSeconds: number;
|
|
245
245
|
}
|
|
246
|
+
/** Options for deriving a JSON metrics snapshot without changing raw counters. */
|
|
247
|
+
interface MetricsSnapshotOptions {
|
|
248
|
+
excludeRoutes?: readonly string[];
|
|
249
|
+
excludeUpstreamPaths?: readonly string[];
|
|
250
|
+
now?: () => number;
|
|
251
|
+
}
|
|
246
252
|
/** JSON body returned by the proxy's `/v1/usage` route. */
|
|
247
253
|
interface UsageResponseBody {
|
|
248
254
|
copilot: CopilotUsage | null;
|
package/dist/index.js
CHANGED
|
@@ -2630,6 +2630,7 @@ var MetricsRegistry = class {
|
|
|
2630
2630
|
#inFlight = 0;
|
|
2631
2631
|
#requests = /* @__PURE__ */ new Map();
|
|
2632
2632
|
#durations = /* @__PURE__ */ new Map();
|
|
2633
|
+
#inFlightByRoute = /* @__PURE__ */ new Map();
|
|
2633
2634
|
#tokens = /* @__PURE__ */ new Map();
|
|
2634
2635
|
#upstream = /* @__PURE__ */ new Map();
|
|
2635
2636
|
#copilotQuota;
|
|
@@ -2639,14 +2640,23 @@ var MetricsRegistry = class {
|
|
|
2639
2640
|
this.#startedAtMs = (options.now ?? Date.now)();
|
|
2640
2641
|
}
|
|
2641
2642
|
/** Mark a request as started; pair with exactly one {@link observe}. */
|
|
2642
|
-
startRequest() {
|
|
2643
|
+
startRequest(route) {
|
|
2643
2644
|
this.#inFlight += 1;
|
|
2645
|
+
if (route) {
|
|
2646
|
+
this.#inFlightByRoute.set(route, (this.#inFlightByRoute.get(route) ?? 0) + 1);
|
|
2647
|
+
}
|
|
2644
2648
|
}
|
|
2645
2649
|
/** Record a completed request and clear its in-flight slot. */
|
|
2646
2650
|
observe(observation) {
|
|
2647
2651
|
if (this.#inFlight > 0) {
|
|
2648
2652
|
this.#inFlight -= 1;
|
|
2649
2653
|
}
|
|
2654
|
+
const inFlightForRoute = this.#inFlightByRoute.get(observation.route) ?? 0;
|
|
2655
|
+
if (inFlightForRoute > 1) {
|
|
2656
|
+
this.#inFlightByRoute.set(observation.route, inFlightForRoute - 1);
|
|
2657
|
+
} else if (inFlightForRoute === 1) {
|
|
2658
|
+
this.#inFlightByRoute.delete(observation.route);
|
|
2659
|
+
}
|
|
2650
2660
|
const key = labelKey(observation.route, observation.method, String(observation.status));
|
|
2651
2661
|
this.#requests.set(key, (this.#requests.get(key) ?? 0) + 1);
|
|
2652
2662
|
this.#observeDuration(observation.route, observation.durationMs / 1e3);
|
|
@@ -2731,12 +2741,19 @@ var MetricsRegistry = class {
|
|
|
2731
2741
|
this.#durations.set(route, entry);
|
|
2732
2742
|
}
|
|
2733
2743
|
/** A JSON-friendly view of the current counters. */
|
|
2734
|
-
snapshot(
|
|
2744
|
+
snapshot(nowOrOptions = Date.now) {
|
|
2745
|
+
const options = typeof nowOrOptions === "function" ? { now: nowOrOptions } : nowOrOptions;
|
|
2746
|
+
const now = options.now ?? Date.now;
|
|
2747
|
+
const excludeRoutes = new Set(options.excludeRoutes ?? []);
|
|
2748
|
+
const excludeUpstreamPaths = new Set(options.excludeUpstreamPaths ?? []);
|
|
2735
2749
|
const byRoute = {};
|
|
2736
2750
|
const byStatus = {};
|
|
2737
2751
|
let requestsTotal = 0;
|
|
2738
2752
|
for (const [key, count] of this.#requests) {
|
|
2739
2753
|
const [route = "", , status = ""] = key.split(LABEL_SEPARATOR);
|
|
2754
|
+
if (excludeRoutes.has(route)) {
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2740
2757
|
byRoute[route] = (byRoute[route] ?? 0) + count;
|
|
2741
2758
|
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
2742
2759
|
requestsTotal += count;
|
|
@@ -2754,8 +2771,12 @@ var MetricsRegistry = class {
|
|
|
2754
2771
|
let upstreamTotal = 0;
|
|
2755
2772
|
let upstreamErrors = 0;
|
|
2756
2773
|
for (const [key, count] of this.#upstream) {
|
|
2774
|
+
const [path = "", outcome = ""] = key.split(LABEL_SEPARATOR);
|
|
2775
|
+
if (excludeUpstreamPaths.has(path)) {
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2757
2778
|
upstreamTotal += count;
|
|
2758
|
-
if (
|
|
2779
|
+
if (outcome === "error") {
|
|
2759
2780
|
upstreamErrors += count;
|
|
2760
2781
|
}
|
|
2761
2782
|
}
|
|
@@ -2765,8 +2786,8 @@ var MetricsRegistry = class {
|
|
|
2765
2786
|
}
|
|
2766
2787
|
return {
|
|
2767
2788
|
githubRateLimit,
|
|
2768
|
-
inFlight: this.#
|
|
2769
|
-
latency: this.#latencySnapshot(),
|
|
2789
|
+
inFlight: this.#filteredInFlight(excludeRoutes),
|
|
2790
|
+
latency: this.#latencySnapshot(excludeRoutes),
|
|
2770
2791
|
requests: { byRoute, byStatus, total: requestsTotal },
|
|
2771
2792
|
startedAt: new Date(this.#startedAtMs).toISOString(),
|
|
2772
2793
|
tokens: { byModel, extraction: { ...this.#extraction }, ...tokenTotals },
|
|
@@ -2774,15 +2795,30 @@ var MetricsRegistry = class {
|
|
|
2774
2795
|
uptimeSeconds: Math.max(0, Math.round((now() - this.#startedAtMs) / 1e3))
|
|
2775
2796
|
};
|
|
2776
2797
|
}
|
|
2798
|
+
#filteredInFlight(excludeRoutes) {
|
|
2799
|
+
if (excludeRoutes.size === 0) {
|
|
2800
|
+
return this.#inFlight;
|
|
2801
|
+
}
|
|
2802
|
+
let excluded = 0;
|
|
2803
|
+
for (const [route, count] of this.#inFlightByRoute) {
|
|
2804
|
+
if (excludeRoutes.has(route)) {
|
|
2805
|
+
excluded += count;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return Math.max(0, this.#inFlight - excluded);
|
|
2809
|
+
}
|
|
2777
2810
|
// Summarize the duration histogram into a JSON latency view: per-route count and
|
|
2778
2811
|
// exact average, plus overall average and estimated p50/p95. The percentiles come
|
|
2779
2812
|
// from the buckets aggregated across routes, so they share /metrics' resolution.
|
|
2780
|
-
#latencySnapshot() {
|
|
2813
|
+
#latencySnapshot(excludeRoutes = /* @__PURE__ */ new Set()) {
|
|
2781
2814
|
const byRoute = {};
|
|
2782
2815
|
const aggregateBuckets = new Array(DURATION_BUCKETS_SECONDS.length).fill(0);
|
|
2783
2816
|
let totalCount = 0;
|
|
2784
2817
|
let totalSum = 0;
|
|
2785
2818
|
for (const [route, entry] of this.#durations) {
|
|
2819
|
+
if (excludeRoutes.has(route)) {
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2786
2822
|
byRoute[route] = {
|
|
2787
2823
|
avgMs: entry.count > 0 ? round2(entry.sum / entry.count * 1e3) : 0,
|
|
2788
2824
|
count: entry.count
|
|
@@ -3813,10 +3849,10 @@ footer.foot .end { margin-left:auto; }
|
|
|
3813
3849
|
pollGen += 1; var myGen = pollGen;
|
|
3814
3850
|
if (inflightFetch){ try { inflightFetch.abort(); } catch (e) {} }
|
|
3815
3851
|
var ctrl = new AbortController(); inflightFetch = ctrl;
|
|
3816
|
-
var to = setTimeout(function(){ try { ctrl.abort(); } catch (e) {} },
|
|
3852
|
+
var to = setTimeout(function(){ try { ctrl.abort(); } catch (e) {} }, Math.max(10000, intervalMs * 2));
|
|
3817
3853
|
var headers = { "accept":"application/json" };
|
|
3818
3854
|
if (apiKey) headers["x-api-key"] = apiKey;
|
|
3819
|
-
fetch("/v1/usage", { headers: headers, signal: ctrl.signal, cache:"no-store" }).then(function(res){
|
|
3855
|
+
fetch("/v1/usage?view=dashboard", { headers: headers, signal: ctrl.signal, cache:"no-store" }).then(function(res){
|
|
3820
3856
|
clearTimeout(to);
|
|
3821
3857
|
if (myGen !== pollGen) return null;
|
|
3822
3858
|
if (res.status === 401 || res.status === 403){ inflightFetch = null; showAuth(!!apiKey); return null; }
|
|
@@ -4127,6 +4163,15 @@ var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
|
4127
4163
|
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
4128
4164
|
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
4129
4165
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
4166
|
+
var DASHBOARD_USAGE_VIEW = "dashboard";
|
|
4167
|
+
var DASHBOARD_EXCLUDED_ROUTES = [
|
|
4168
|
+
"cors.preflight",
|
|
4169
|
+
"dashboard",
|
|
4170
|
+
"health",
|
|
4171
|
+
"metrics",
|
|
4172
|
+
"usage"
|
|
4173
|
+
];
|
|
4174
|
+
var DASHBOARD_EXCLUDED_UPSTREAM_PATHS = ["/copilot_internal/user"];
|
|
4130
4175
|
var RequestBodyTooLargeError = class extends Error {
|
|
4131
4176
|
constructor() {
|
|
4132
4177
|
super(REQUEST_TOO_LARGE_MESSAGE);
|
|
@@ -4179,7 +4224,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
4179
4224
|
requestId,
|
|
4180
4225
|
route
|
|
4181
4226
|
});
|
|
4182
|
-
metrics.startRequest();
|
|
4227
|
+
metrics.startRequest(route);
|
|
4183
4228
|
const origin = request.headers.get("origin")?.trim() || void 0;
|
|
4184
4229
|
const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
|
|
4185
4230
|
const inner = normalizeInnerRequest(request, apiPath, url);
|
|
@@ -4302,7 +4347,7 @@ function buildApp(deps) {
|
|
|
4302
4347
|
}
|
|
4303
4348
|
logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
|
|
4304
4349
|
return jsonError(500, "internal_error", message);
|
|
4305
|
-
}).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
|
|
4350
|
+
}).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(
|
|
4306
4351
|
"/v1/models",
|
|
4307
4352
|
({ request }) => handleModels(client, metrics, request.signal, loggerFor(request))
|
|
4308
4353
|
).get("/v1/responses", () => websocketUnsupportedResponse()).post(
|
|
@@ -5060,9 +5105,13 @@ function dashboardResponse() {
|
|
|
5060
5105
|
status: 200
|
|
5061
5106
|
});
|
|
5062
5107
|
}
|
|
5063
|
-
async function handleUsage(metrics, readUsage,
|
|
5064
|
-
const
|
|
5065
|
-
const
|
|
5108
|
+
async function handleUsage(metrics, readUsage, request) {
|
|
5109
|
+
const view = new URL(request.url).searchParams.get("view");
|
|
5110
|
+
const { copilot, error } = await readUsage(request.signal);
|
|
5111
|
+
const proxy = view === DASHBOARD_USAGE_VIEW ? metrics.snapshot({
|
|
5112
|
+
excludeRoutes: DASHBOARD_EXCLUDED_ROUTES,
|
|
5113
|
+
excludeUpstreamPaths: DASHBOARD_EXCLUDED_UPSTREAM_PATHS
|
|
5114
|
+
}) : metrics.snapshot();
|
|
5066
5115
|
const body = {
|
|
5067
5116
|
copilot: copilot ?? null,
|
|
5068
5117
|
object: "usage",
|
|
@@ -5079,25 +5128,30 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
|
|
|
5079
5128
|
let cache;
|
|
5080
5129
|
return async (signal) => {
|
|
5081
5130
|
if (cache && now() - cache.atMs < ttlMs) {
|
|
5082
|
-
return
|
|
5131
|
+
return cache.result;
|
|
5083
5132
|
}
|
|
5084
5133
|
try {
|
|
5085
5134
|
const upstream = await client.usage(signal);
|
|
5086
5135
|
metrics.recordUpstream(usagePath, upstream.ok);
|
|
5087
5136
|
metrics.recordGithubRateLimit(parseRateLimitHeaders(upstream.headers, now()));
|
|
5088
5137
|
if (!upstream.ok) {
|
|
5089
|
-
|
|
5138
|
+
const result2 = { error: `GitHub Copilot usage request failed with ${upstream.status}.` };
|
|
5139
|
+
cache = { atMs: now(), result: result2 };
|
|
5140
|
+
return result2;
|
|
5090
5141
|
}
|
|
5091
5142
|
const value = normalizeCopilotUsage(await upstream.json().catch(() => ({})));
|
|
5092
|
-
|
|
5143
|
+
const result = { copilot: value };
|
|
5144
|
+
cache = { atMs: now(), result };
|
|
5093
5145
|
metrics.recordCopilotQuota(value);
|
|
5094
|
-
return
|
|
5146
|
+
return result;
|
|
5095
5147
|
} catch (error) {
|
|
5096
5148
|
if (error instanceof CopilotAuthError) {
|
|
5097
5149
|
return { error: error.message };
|
|
5098
5150
|
}
|
|
5099
5151
|
metrics.recordUpstream(usagePath, false);
|
|
5100
|
-
|
|
5152
|
+
const result = { error: errorMessage(error) };
|
|
5153
|
+
cache = { atMs: now(), result };
|
|
5154
|
+
return result;
|
|
5101
5155
|
}
|
|
5102
5156
|
};
|
|
5103
5157
|
}
|