@openhoo/hoopilot 0.10.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 +225 -150
- package/dist/{chunk-7GSQVYYT.js → chunk-JU6F5L34.js} +9 -5
- package/dist/chunk-JU6F5L34.js.map +1 -0
- package/dist/cli.js +212 -22
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.cjs +190 -21
- 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 +189 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7GSQVYYT.js.map +0 -1
package/dist/codexx.js
CHANGED
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
|
}
|
|
@@ -2694,7 +2820,8 @@ var IS_STANDALONE_BINARY = BAKED_VERSION !== void 0;
|
|
|
2694
2820
|
// src/server.ts
|
|
2695
2821
|
var DEFAULT_HOST = "127.0.0.1";
|
|
2696
2822
|
var DEFAULT_PORT = 4141;
|
|
2697
|
-
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "
|
|
2823
|
+
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Cross-origin browser requests are blocked unless the Origin is loopback or listed in HOOPILOT_ALLOWED_ORIGINS.";
|
|
2824
|
+
var WELL_KNOWN_DEMO_API_KEYS = /* @__PURE__ */ new Set(["local-key"]);
|
|
2698
2825
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
2699
2826
|
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
2700
2827
|
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
@@ -2710,6 +2837,7 @@ var RequestBodyTooLargeError = class extends Error {
|
|
|
2710
2837
|
function createHoopilotHandler(options = {}) {
|
|
2711
2838
|
const client = new CopilotClient(options);
|
|
2712
2839
|
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
2840
|
+
const allowedOrigins = parseAllowedOrigins(options.env);
|
|
2713
2841
|
const logger = serverLogger(options);
|
|
2714
2842
|
const metrics = options.metrics ?? new MetricsRegistry();
|
|
2715
2843
|
const readUsage = createUsageReader(client, metrics);
|
|
@@ -2729,7 +2857,10 @@ function createHoopilotHandler(options = {}) {
|
|
|
2729
2857
|
route
|
|
2730
2858
|
});
|
|
2731
2859
|
metrics.startRequest();
|
|
2860
|
+
const origin = request.headers.get("origin")?.trim() || void 0;
|
|
2861
|
+
const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
|
|
2732
2862
|
const finish = (response) => finishResponse(response, {
|
|
2863
|
+
corsOrigin,
|
|
2733
2864
|
logger: requestLogger,
|
|
2734
2865
|
method: request.method,
|
|
2735
2866
|
metrics,
|
|
@@ -2739,11 +2870,11 @@ function createHoopilotHandler(options = {}) {
|
|
|
2739
2870
|
closeConnection: bufferProxyBodies,
|
|
2740
2871
|
trackStreamingBody: !bufferProxyBodies
|
|
2741
2872
|
});
|
|
2742
|
-
const browserOrigin = forbiddenBrowserOrigin(request,
|
|
2873
|
+
const browserOrigin = forbiddenBrowserOrigin(origin, request, allowedOrigins);
|
|
2743
2874
|
if (browserOrigin) {
|
|
2744
2875
|
requestLogger.warn(
|
|
2745
2876
|
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
2746
|
-
"blocked
|
|
2877
|
+
"blocked cross-origin browser request"
|
|
2747
2878
|
);
|
|
2748
2879
|
return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
|
|
2749
2880
|
}
|
|
@@ -2869,10 +3000,17 @@ function startHoopilotServer(options = {}) {
|
|
|
2869
3000
|
const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
|
|
2870
3001
|
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
2871
3002
|
const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
|
|
2872
|
-
if (!isLoopbackHost(host)
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
3003
|
+
if (!isLoopbackHost(host)) {
|
|
3004
|
+
if (!apiKey && !allowUnauthenticated) {
|
|
3005
|
+
throw new Error(
|
|
3006
|
+
"Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
if (apiKey && isWellKnownDemoApiKey(apiKey)) {
|
|
3010
|
+
throw new Error(
|
|
3011
|
+
"Refusing to listen on a non-loopback host with a well-known demo HOOPILOT_API_KEY. Set a strong, unique API key."
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
2876
3014
|
}
|
|
2877
3015
|
const server = Bun.serve({
|
|
2878
3016
|
fetch: createHoopilotHandler({
|
|
@@ -3185,7 +3323,6 @@ function corsHeaders() {
|
|
|
3185
3323
|
return {
|
|
3186
3324
|
"access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
|
|
3187
3325
|
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
3188
|
-
"access-control-allow-origin": "*",
|
|
3189
3326
|
"access-control-expose-headers": "x-request-id"
|
|
3190
3327
|
};
|
|
3191
3328
|
}
|
|
@@ -3197,17 +3334,34 @@ function isAuthorized(request, apiKey) {
|
|
|
3197
3334
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
3198
3335
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
3199
3336
|
}
|
|
3200
|
-
function forbiddenBrowserOrigin(request,
|
|
3201
|
-
if (apiKey) {
|
|
3202
|
-
return void 0;
|
|
3203
|
-
}
|
|
3204
|
-
const origin = request.headers.get("origin")?.trim();
|
|
3337
|
+
function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
|
|
3205
3338
|
if (origin) {
|
|
3206
|
-
return
|
|
3339
|
+
return isAllowedOrigin(origin, allowedOrigins) ? void 0 : origin;
|
|
3207
3340
|
}
|
|
3208
3341
|
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
3209
3342
|
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
3210
3343
|
}
|
|
3344
|
+
function parseAllowedOrigins(env) {
|
|
3345
|
+
const raw = envValue(env?.HOOPILOT_ALLOWED_ORIGINS);
|
|
3346
|
+
if (!raw) {
|
|
3347
|
+
return /* @__PURE__ */ new Set();
|
|
3348
|
+
}
|
|
3349
|
+
return new Set(
|
|
3350
|
+
raw.split(",").map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)
|
|
3351
|
+
);
|
|
3352
|
+
}
|
|
3353
|
+
function isAllowedOrigin(origin, allowedOrigins) {
|
|
3354
|
+
return isLoopbackOrigin(origin) || allowedOrigins.has(origin.toLowerCase());
|
|
3355
|
+
}
|
|
3356
|
+
function resolveCorsAllowOrigin(origin, allowedOrigins) {
|
|
3357
|
+
if (!origin) {
|
|
3358
|
+
return "*";
|
|
3359
|
+
}
|
|
3360
|
+
return isAllowedOrigin(origin, allowedOrigins) ? origin : void 0;
|
|
3361
|
+
}
|
|
3362
|
+
function isWellKnownDemoApiKey(apiKey) {
|
|
3363
|
+
return WELL_KNOWN_DEMO_API_KEYS.has(apiKey.trim().toLowerCase());
|
|
3364
|
+
}
|
|
3211
3365
|
function isUpstreamAuthStatus(status) {
|
|
3212
3366
|
return status === 401 || status === 403;
|
|
3213
3367
|
}
|
|
@@ -3267,7 +3421,12 @@ function shouldBufferProxyBodies(mode) {
|
|
|
3267
3421
|
return process.platform === "win32" && IS_STANDALONE_BINARY;
|
|
3268
3422
|
}
|
|
3269
3423
|
function finishResponse(response, options) {
|
|
3270
|
-
const withRequestId = responseWithRequestId(
|
|
3424
|
+
const withRequestId = responseWithRequestId(
|
|
3425
|
+
response,
|
|
3426
|
+
options.requestId,
|
|
3427
|
+
options.closeConnection,
|
|
3428
|
+
options.corsOrigin
|
|
3429
|
+
);
|
|
3271
3430
|
const stream = isStreamingResponse(withRequestId);
|
|
3272
3431
|
const status = withRequestId.status;
|
|
3273
3432
|
const complete = () => {
|
|
@@ -3285,9 +3444,17 @@ function finishResponse(response, options) {
|
|
|
3285
3444
|
complete();
|
|
3286
3445
|
return withRequestId;
|
|
3287
3446
|
}
|
|
3288
|
-
function responseWithRequestId(response, requestId, closeConnection) {
|
|
3447
|
+
function responseWithRequestId(response, requestId, closeConnection, corsOrigin) {
|
|
3289
3448
|
const headers = new Headers(response.headers);
|
|
3290
3449
|
headers.set("x-request-id", requestId);
|
|
3450
|
+
if (corsOrigin) {
|
|
3451
|
+
headers.set("access-control-allow-origin", corsOrigin);
|
|
3452
|
+
if (corsOrigin !== "*") {
|
|
3453
|
+
headers.append("vary", "Origin");
|
|
3454
|
+
}
|
|
3455
|
+
} else {
|
|
3456
|
+
headers.delete("access-control-allow-origin");
|
|
3457
|
+
}
|
|
3291
3458
|
if (closeConnection) {
|
|
3292
3459
|
headers.set("connection", "close");
|
|
3293
3460
|
}
|
|
@@ -3451,6 +3618,7 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
|
|
|
3451
3618
|
try {
|
|
3452
3619
|
const upstream = await client.usage(signal);
|
|
3453
3620
|
metrics.recordUpstream(usagePath, upstream.ok);
|
|
3621
|
+
metrics.recordGithubRateLimit(parseRateLimitHeaders(upstream.headers, now()));
|
|
3454
3622
|
if (!upstream.ok) {
|
|
3455
3623
|
return { error: `GitHub Copilot usage request failed with ${upstream.status}.` };
|
|
3456
3624
|
}
|
|
@@ -3509,6 +3677,7 @@ function safeParseJson(text) {
|
|
|
3509
3677
|
observeResponseUsage,
|
|
3510
3678
|
parseLogFormat,
|
|
3511
3679
|
parseLogLevel,
|
|
3680
|
+
parseRateLimitHeaders,
|
|
3512
3681
|
readStoredCopilotAuth,
|
|
3513
3682
|
responsesCompactionResult,
|
|
3514
3683
|
responsesRequestToChatCompletion,
|