@openhoo/hoopilot 1.0.0 → 1.1.0
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 +6 -4
- package/dist/cli.js +154 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +133 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +132 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -63,6 +63,7 @@ __export(index_exports, {
|
|
|
63
63
|
observeResponseUsage: () => observeResponseUsage,
|
|
64
64
|
parseLogFormat: () => parseLogFormat,
|
|
65
65
|
parseLogLevel: () => parseLogLevel,
|
|
66
|
+
parseRateLimitHeaders: () => parseRateLimitHeaders,
|
|
66
67
|
readStoredCopilotAuth: () => readStoredCopilotAuth,
|
|
67
68
|
responsesCompactionResult: () => responsesCompactionResult,
|
|
68
69
|
responsesRequestToChatCompletion: () => responsesRequestToChatCompletion,
|
|
@@ -1833,6 +1834,38 @@ function applyGithubApiHeaders(headers, token) {
|
|
|
1833
1834
|
headers.set("x-github-api-version", COPILOT_USAGE_API_VERSION);
|
|
1834
1835
|
return headers;
|
|
1835
1836
|
}
|
|
1837
|
+
function parseRateLimitHeaders(headers, nowMs = Date.now()) {
|
|
1838
|
+
const limit = headerInt(headers, "x-ratelimit-limit");
|
|
1839
|
+
const remaining = headerInt(headers, "x-ratelimit-remaining");
|
|
1840
|
+
const used = headerInt(headers, "x-ratelimit-used");
|
|
1841
|
+
const resetEpochSeconds = headerInt(headers, "x-ratelimit-reset");
|
|
1842
|
+
const retryAfterSeconds = headerInt(headers, "retry-after");
|
|
1843
|
+
if (limit === void 0 && remaining === void 0 && used === void 0 && resetEpochSeconds === void 0 && retryAfterSeconds === void 0) {
|
|
1844
|
+
return void 0;
|
|
1845
|
+
}
|
|
1846
|
+
return removeUndefinedRateLimit({
|
|
1847
|
+
limit,
|
|
1848
|
+
observedAtMs: nowMs,
|
|
1849
|
+
remaining,
|
|
1850
|
+
resetEpochSeconds,
|
|
1851
|
+
resource: headers.get("x-ratelimit-resource")?.trim() || "unknown",
|
|
1852
|
+
retryAfterSeconds,
|
|
1853
|
+
used
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
function headerInt(headers, name) {
|
|
1857
|
+
const raw = headers.get(name);
|
|
1858
|
+
if (raw === null) {
|
|
1859
|
+
return void 0;
|
|
1860
|
+
}
|
|
1861
|
+
const value = Number.parseInt(raw.trim(), 10);
|
|
1862
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
1863
|
+
}
|
|
1864
|
+
function removeUndefinedRateLimit(rateLimit) {
|
|
1865
|
+
return Object.fromEntries(
|
|
1866
|
+
Object.entries(rateLimit).filter(([, value]) => value !== void 0)
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1836
1869
|
var CopilotClient = class {
|
|
1837
1870
|
#auth;
|
|
1838
1871
|
#allowUnsafeUpstream;
|
|
@@ -2249,6 +2282,7 @@ var DURATION_BUCKETS_SECONDS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
|
|
|
2249
2282
|
var USAGE_BUFFER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
2250
2283
|
var MAX_TRACKED_MODELS = 200;
|
|
2251
2284
|
var MAX_MODEL_LABEL_LENGTH = 200;
|
|
2285
|
+
var MAX_TRACKED_RATELIMIT_RESOURCES = 32;
|
|
2252
2286
|
var LABEL_SEPARATOR = "";
|
|
2253
2287
|
var UNKNOWN_MODEL = "unknown";
|
|
2254
2288
|
function emptyModelTotals() {
|
|
@@ -2262,6 +2296,7 @@ var MetricsRegistry = class {
|
|
|
2262
2296
|
#tokens = /* @__PURE__ */ new Map();
|
|
2263
2297
|
#upstream = /* @__PURE__ */ new Map();
|
|
2264
2298
|
#copilotQuota;
|
|
2299
|
+
#githubRateLimit = /* @__PURE__ */ new Map();
|
|
2265
2300
|
constructor(options = {}) {
|
|
2266
2301
|
this.#startedAtMs = (options.now ?? Date.now)();
|
|
2267
2302
|
}
|
|
@@ -2299,17 +2334,39 @@ var MetricsRegistry = class {
|
|
|
2299
2334
|
recordCopilotQuota(usage) {
|
|
2300
2335
|
this.#copilotQuota = usage;
|
|
2301
2336
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2337
|
+
/**
|
|
2338
|
+
* Store the latest GitHub REST rate-limit budget, keyed by its resource bucket.
|
|
2339
|
+
* A no-op when `rateLimit` is undefined (the response carried no rate-limit
|
|
2340
|
+
* headers) so callers can pass {@link parseRateLimitHeaders} output directly.
|
|
2341
|
+
*/
|
|
2342
|
+
recordGithubRateLimit(rateLimit) {
|
|
2343
|
+
if (!rateLimit) {
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const resource = this.#rateLimitResource(rateLimit.resource);
|
|
2347
|
+
this.#githubRateLimit.set(resource, { ...rateLimit, resource });
|
|
2348
|
+
}
|
|
2349
|
+
// Sanitize the model into a bounded label. The model can originate from a
|
|
2350
|
+
// client request, so cap its length, strip characters that would corrupt the
|
|
2351
|
+
// exposition format, and fold overflow past the cardinality limit into
|
|
2352
|
+
// UNKNOWN_MODEL to keep the series count bounded.
|
|
2306
2353
|
#modelLabel(model) {
|
|
2307
|
-
const cleaned = model
|
|
2354
|
+
const cleaned = cleanLabel(model).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
|
|
2308
2355
|
if (!this.#tokens.has(cleaned) && this.#tokens.size >= MAX_TRACKED_MODELS) {
|
|
2309
2356
|
return UNKNOWN_MODEL;
|
|
2310
2357
|
}
|
|
2311
2358
|
return cleaned;
|
|
2312
2359
|
}
|
|
2360
|
+
// The resource comes from a trusted upstream header, but clean and bound it
|
|
2361
|
+
// with the same discipline as model labels: strip control characters that
|
|
2362
|
+
// would corrupt the exposition format and fold overflow into "unknown".
|
|
2363
|
+
#rateLimitResource(resource) {
|
|
2364
|
+
const cleaned = cleanLabel(resource).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
|
|
2365
|
+
if (!this.#githubRateLimit.has(cleaned) && this.#githubRateLimit.size >= MAX_TRACKED_RATELIMIT_RESOURCES) {
|
|
2366
|
+
return UNKNOWN_MODEL;
|
|
2367
|
+
}
|
|
2368
|
+
return cleaned;
|
|
2369
|
+
}
|
|
2313
2370
|
#observeDuration(route, seconds) {
|
|
2314
2371
|
const value = Number.isFinite(seconds) && seconds >= 0 ? seconds : 0;
|
|
2315
2372
|
const entry = this.#durations.get(route) ?? {
|
|
@@ -2354,7 +2411,12 @@ var MetricsRegistry = class {
|
|
|
2354
2411
|
upstreamErrors += count;
|
|
2355
2412
|
}
|
|
2356
2413
|
}
|
|
2414
|
+
const githubRateLimit = {};
|
|
2415
|
+
for (const [resource, rateLimit] of this.#githubRateLimit) {
|
|
2416
|
+
githubRateLimit[resource] = toRateLimitSnapshot(rateLimit);
|
|
2417
|
+
}
|
|
2357
2418
|
return {
|
|
2419
|
+
githubRateLimit,
|
|
2358
2420
|
inFlight: this.#inFlight,
|
|
2359
2421
|
requests: { byRoute, byStatus, total: requestsTotal },
|
|
2360
2422
|
startedAt: new Date(this.#startedAtMs).toISOString(),
|
|
@@ -2425,10 +2487,43 @@ var MetricsRegistry = class {
|
|
|
2425
2487
|
lines.push(`hoopilot_request_duration_seconds_sum${labels({ route })} ${entry.sum}`);
|
|
2426
2488
|
lines.push(`hoopilot_request_duration_seconds_count${labels({ route })} ${entry.count}`);
|
|
2427
2489
|
}
|
|
2490
|
+
this.#renderGithubRateLimit(lines);
|
|
2428
2491
|
this.#renderCopilotQuota(lines);
|
|
2429
2492
|
return `${lines.join("\n")}
|
|
2430
2493
|
`;
|
|
2431
2494
|
}
|
|
2495
|
+
#renderGithubRateLimit(lines) {
|
|
2496
|
+
const entries = [...this.#githubRateLimit.values()];
|
|
2497
|
+
if (entries.length === 0) {
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
const gauge = (suffix, help, pick) => {
|
|
2501
|
+
const present = entries.filter((rateLimit) => pick(rateLimit) !== void 0);
|
|
2502
|
+
if (present.length === 0) {
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
lines.push(`# HELP hoopilot_github_ratelimit_${suffix} ${help}`);
|
|
2506
|
+
lines.push(`# TYPE hoopilot_github_ratelimit_${suffix} gauge`);
|
|
2507
|
+
for (const rateLimit of present) {
|
|
2508
|
+
lines.push(
|
|
2509
|
+
`hoopilot_github_ratelimit_${suffix}${labels({ resource: rateLimit.resource })} ${pick(rateLimit)}`
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
gauge("limit", "GitHub REST API request ceiling for the resource window.", (r) => r.limit);
|
|
2514
|
+
gauge("remaining", "Requests remaining in the GitHub REST API window.", (r) => r.remaining);
|
|
2515
|
+
gauge("used", "Requests used in the GitHub REST API window.", (r) => r.used);
|
|
2516
|
+
gauge(
|
|
2517
|
+
"reset_timestamp_seconds",
|
|
2518
|
+
"Unix epoch when the GitHub REST API window resets.",
|
|
2519
|
+
(r) => r.resetEpochSeconds
|
|
2520
|
+
);
|
|
2521
|
+
gauge(
|
|
2522
|
+
"retry_after_seconds",
|
|
2523
|
+
"Seconds to wait after a GitHub secondary-limit response.",
|
|
2524
|
+
(r) => r.retryAfterSeconds
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2432
2527
|
#renderCopilotQuota(lines) {
|
|
2433
2528
|
const usage = this.#copilotQuota;
|
|
2434
2529
|
if (!usage) {
|
|
@@ -2669,6 +2764,37 @@ function modelText(value) {
|
|
|
2669
2764
|
function nonNegative(value) {
|
|
2670
2765
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
2671
2766
|
}
|
|
2767
|
+
function cleanLabel(value) {
|
|
2768
|
+
let result = "";
|
|
2769
|
+
for (const char of value) {
|
|
2770
|
+
const code = char.charCodeAt(0);
|
|
2771
|
+
if (code > 31 && code !== 127) {
|
|
2772
|
+
result += char;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return result.trim();
|
|
2776
|
+
}
|
|
2777
|
+
function toRateLimitSnapshot(rateLimit) {
|
|
2778
|
+
const snapshot = {
|
|
2779
|
+
observedAt: new Date(rateLimit.observedAtMs).toISOString()
|
|
2780
|
+
};
|
|
2781
|
+
if (rateLimit.limit !== void 0) {
|
|
2782
|
+
snapshot.limit = rateLimit.limit;
|
|
2783
|
+
}
|
|
2784
|
+
if (rateLimit.remaining !== void 0) {
|
|
2785
|
+
snapshot.remaining = rateLimit.remaining;
|
|
2786
|
+
}
|
|
2787
|
+
if (rateLimit.used !== void 0) {
|
|
2788
|
+
snapshot.used = rateLimit.used;
|
|
2789
|
+
}
|
|
2790
|
+
if (rateLimit.resetEpochSeconds !== void 0) {
|
|
2791
|
+
snapshot.resetAt = new Date(rateLimit.resetEpochSeconds * 1e3).toISOString();
|
|
2792
|
+
}
|
|
2793
|
+
if (rateLimit.retryAfterSeconds !== void 0) {
|
|
2794
|
+
snapshot.retryAfterSeconds = rateLimit.retryAfterSeconds;
|
|
2795
|
+
}
|
|
2796
|
+
return snapshot;
|
|
2797
|
+
}
|
|
2672
2798
|
function labelKey(...parts) {
|
|
2673
2799
|
return parts.join(LABEL_SEPARATOR);
|
|
2674
2800
|
}
|
|
@@ -3492,6 +3618,7 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
|
|
|
3492
3618
|
try {
|
|
3493
3619
|
const upstream = await client.usage(signal);
|
|
3494
3620
|
metrics.recordUpstream(usagePath, upstream.ok);
|
|
3621
|
+
metrics.recordGithubRateLimit(parseRateLimitHeaders(upstream.headers, now()));
|
|
3495
3622
|
if (!upstream.ok) {
|
|
3496
3623
|
return { error: `GitHub Copilot usage request failed with ${upstream.status}.` };
|
|
3497
3624
|
}
|
|
@@ -3550,6 +3677,7 @@ function safeParseJson(text) {
|
|
|
3550
3677
|
observeResponseUsage,
|
|
3551
3678
|
parseLogFormat,
|
|
3552
3679
|
parseLogLevel,
|
|
3680
|
+
parseRateLimitHeaders,
|
|
3553
3681
|
readStoredCopilotAuth,
|
|
3554
3682
|
responsesCompactionResult,
|
|
3555
3683
|
responsesRequestToChatCompletion,
|