aisubs 0.4.0 → 0.4.1
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/CHANGELOG.md +9 -0
- package/dist/auth.d.ts +11 -2
- package/dist/auth.js +42 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/chatgpt.js +2 -1
- package/dist/providers/copilot.js +2 -1
- package/dist/types.d.ts +5 -0
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +82 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 - 2026-10-06
|
|
4
|
+
|
|
5
|
+
- Add `SubscriptionAuth.onUsage(listener)`: usage that provider responses report about
|
|
6
|
+
themselves, with no extra request. ChatGPT reads its `x-codex-*` rate-limit headers and
|
|
7
|
+
GitHub Copilot its `x-quota-snapshot-*` headers; the cached usage snapshot follows along.
|
|
8
|
+
- Add the `usageFromResponse` adapter hook, plus `parseChatGptUsageHeaders`,
|
|
9
|
+
`parseCopilotUsageHeaders` and `mergeObservedUsage`.
|
|
10
|
+
- `getUsage` (and `SubscriptionAccount.getUsage`) take `force` to bypass the usage cache.
|
|
11
|
+
|
|
3
12
|
## 0.4.0 - 2026-10-06
|
|
4
13
|
|
|
5
14
|
- Breaking: removed the Claude subscription provider, the `aisubs/providers/claude`
|
package/dist/auth.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export interface SubscriptionAccount {
|
|
|
18
18
|
getAccessToken(): Promise<string>;
|
|
19
19
|
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
20
20
|
proxy(path: string, init?: RequestInit): Promise<Response>;
|
|
21
|
-
|
|
21
|
+
/** `force` reads the provider now instead of answering from the short usage cache. */
|
|
22
|
+
getUsage(signal?: AbortSignal, force?: boolean): Promise<ProviderUsage | null>;
|
|
22
23
|
getModels(signal?: AbortSignal, force?: boolean): Promise<ProviderModels | null>;
|
|
23
24
|
credentialSummary(): Promise<CredentialSummary>;
|
|
24
25
|
details(signal?: AbortSignal): Promise<SubscriptionAccountDetails>;
|
|
@@ -35,6 +36,7 @@ export declare class SubscriptionAuth {
|
|
|
35
36
|
private readonly usageInflight;
|
|
36
37
|
private readonly modelsInflight;
|
|
37
38
|
private readonly metadataGenerations;
|
|
39
|
+
private readonly usageListeners;
|
|
38
40
|
private readonly refreshTimeoutMs;
|
|
39
41
|
private readonly usageCacheTtlMs;
|
|
40
42
|
private readonly modelsCacheTtlMs;
|
|
@@ -67,11 +69,18 @@ export declare class SubscriptionAuth {
|
|
|
67
69
|
getAccessToken(provider: ProviderId, account?: string): Promise<string>;
|
|
68
70
|
credentialSummary(provider: ProviderId, account?: string): Promise<CredentialSummary>;
|
|
69
71
|
details(provider: ProviderId, account?: string, signal?: AbortSignal): Promise<SubscriptionAccountDetails>;
|
|
72
|
+
/**
|
|
73
|
+
* Calls `listener` with the usage each provider response reports about itself (for example
|
|
74
|
+
* ChatGPT's rate-limit headers): only the meters that response mentions, as of its arrival.
|
|
75
|
+
* The cached usage snapshot is updated too. Returns the unsubscribe function.
|
|
76
|
+
*/
|
|
77
|
+
onUsage(listener: (usage: ProviderUsage) => void): () => void;
|
|
78
|
+
private observeUsage;
|
|
70
79
|
fetch(provider: ProviderId, input: string | URL | Request, init?: RequestInit, account?: string): Promise<Response>;
|
|
71
80
|
proxy(provider: ProviderId, account: string, path: string, init?: RequestInit): Promise<Response>;
|
|
72
81
|
/** Build an authorized direct-provider request for transports such as WebSocket. */
|
|
73
82
|
authorizeProxyRequest(provider: ProviderId, account: string, path: string, init?: RequestInit): Promise<Request>;
|
|
74
|
-
getUsage(provider: ProviderId, account?: string, callerSignal?: AbortSignal): Promise<ProviderUsage | null>;
|
|
83
|
+
getUsage(provider: ProviderId, account?: string, callerSignal?: AbortSignal, force?: boolean): Promise<ProviderUsage | null>;
|
|
75
84
|
getModels(provider: ProviderId, account?: string, callerSignal?: AbortSignal, force?: boolean): Promise<ProviderModels | null>;
|
|
76
85
|
account(provider: ProviderId, account: string): SubscriptionAccount;
|
|
77
86
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeObservedUsage } from "./usage.js";
|
|
1
2
|
import { errorMessage } from "./utils.js";
|
|
2
3
|
export const DEFAULT_ACCOUNT = "default";
|
|
3
4
|
const ACCOUNT_STORAGE_PREFIX = "$subscription-account$";
|
|
@@ -73,6 +74,7 @@ export class SubscriptionAuth {
|
|
|
73
74
|
usageInflight = new Map();
|
|
74
75
|
modelsInflight = new Map();
|
|
75
76
|
metadataGenerations = new Map();
|
|
77
|
+
usageListeners = new Set();
|
|
76
78
|
refreshTimeoutMs;
|
|
77
79
|
usageCacheTtlMs;
|
|
78
80
|
modelsCacheTtlMs;
|
|
@@ -498,6 +500,38 @@ export class SubscriptionAuth {
|
|
|
498
500
|
]);
|
|
499
501
|
return { session, credential, usage, models };
|
|
500
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Calls `listener` with the usage each provider response reports about itself (for example
|
|
505
|
+
* ChatGPT's rate-limit headers): only the meters that response mentions, as of its arrival.
|
|
506
|
+
* The cached usage snapshot is updated too. Returns the unsubscribe function.
|
|
507
|
+
*/
|
|
508
|
+
onUsage(listener) {
|
|
509
|
+
this.usageListeners.add(listener);
|
|
510
|
+
return () => this.usageListeners.delete(listener);
|
|
511
|
+
}
|
|
512
|
+
observeUsage(adapter, provider, accountKey, response) {
|
|
513
|
+
let data;
|
|
514
|
+
try {
|
|
515
|
+
data = adapter.usageFromResponse?.(response) ?? null;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!data)
|
|
521
|
+
return;
|
|
522
|
+
const usage = { provider, accountKey, asOf: Date.now(), ...data };
|
|
523
|
+
const scope = credentialKey(provider, accountKey);
|
|
524
|
+
const cached = this.usageCache.get(scope);
|
|
525
|
+
if (cached?.value) {
|
|
526
|
+
cached.value = { ...mergeObservedUsage(cached.value, usage), asOf: usage.asOf };
|
|
527
|
+
}
|
|
528
|
+
for (const listener of this.usageListeners) {
|
|
529
|
+
try {
|
|
530
|
+
listener(usage);
|
|
531
|
+
}
|
|
532
|
+
catch { }
|
|
533
|
+
}
|
|
534
|
+
}
|
|
501
535
|
async fetch(provider, input, init, account = DEFAULT_ACCOUNT) {
|
|
502
536
|
const adapter = this.adapter(provider);
|
|
503
537
|
const original = createRequest(input, init);
|
|
@@ -509,6 +543,7 @@ export class SubscriptionAuth {
|
|
|
509
543
|
headers.set("cache-control", "no-store");
|
|
510
544
|
const request = new Request(authorized, { cache: "no-store", headers });
|
|
511
545
|
const response = await globalThis.fetch(request);
|
|
546
|
+
this.observeUsage(adapter, provider, accountKey, response);
|
|
512
547
|
return adapter.normalizeResponse?.(request, response) ?? response;
|
|
513
548
|
};
|
|
514
549
|
if (adapter.supportsFetch === false) {
|
|
@@ -560,12 +595,17 @@ export class SubscriptionAuth {
|
|
|
560
595
|
const normalized = (await adapter.normalizeRequest?.(request.clone())) ?? request;
|
|
561
596
|
return adapter.authorize(normalized, credential);
|
|
562
597
|
}
|
|
563
|
-
async getUsage(provider, account = DEFAULT_ACCOUNT, callerSignal) {
|
|
598
|
+
async getUsage(provider, account = DEFAULT_ACCOUNT, callerSignal, force = false) {
|
|
564
599
|
const adapter = this.adapter(provider);
|
|
565
600
|
if (!adapter.getUsage)
|
|
566
601
|
return null;
|
|
567
602
|
const accountKey = normalizeAccountKey(account);
|
|
568
603
|
const scope = credentialKey(provider, accountKey);
|
|
604
|
+
if (force) {
|
|
605
|
+
this.usageCache.delete(scope);
|
|
606
|
+
this.usageInflight.delete(scope);
|
|
607
|
+
this.metadataGenerations.set(scope, (this.metadataGenerations.get(scope) ?? 0) + 1);
|
|
608
|
+
}
|
|
569
609
|
return this.cachedMetadata(this.usageCache, this.usageInflight, scope, this.usageCacheTtlMs, callerSignal, async () => {
|
|
570
610
|
const credential = await this.credential(provider, accountKey);
|
|
571
611
|
const timeout = AbortSignal.timeout(this.refreshTimeoutMs);
|
|
@@ -613,7 +653,7 @@ export class SubscriptionAuth {
|
|
|
613
653
|
getAccessToken: () => this.getAccessToken(provider, accountKey),
|
|
614
654
|
fetch: (input, init) => this.fetch(provider, input, init, accountKey),
|
|
615
655
|
proxy: (path, init) => this.proxy(provider, accountKey, path, init),
|
|
616
|
-
getUsage: (signal) => this.getUsage(provider, accountKey, signal),
|
|
656
|
+
getUsage: (signal, force) => this.getUsage(provider, accountKey, signal, force),
|
|
617
657
|
getModels: (signal, force) => this.getModels(provider, accountKey, signal, force),
|
|
618
658
|
credentialSummary: () => this.credentialSummary(provider, accountKey),
|
|
619
659
|
details: (signal) => this.details(provider, accountKey, signal),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, type SubscriptionAccount, type SubscriptionAuthOptions, } from "./auth.js";
|
|
2
2
|
export { MemoryCredentialStore } from "./memory-store.js";
|
|
3
|
-
export { parseChatGptUsage, parseCopilotUsage, parseGrokUsage } from "./usage.js";
|
|
3
|
+
export { mergeObservedUsage, parseChatGptUsage, parseChatGptUsageHeaders, parseCopilotUsage, parseCopilotUsageHeaders, parseGrokUsage, } from "./usage.js";
|
|
4
4
|
export type { CredentialStore, CoordinatedCredentialStore, CredentialSummary, BrowserLoginPrompt, DeviceLoginPrompt, ImmediateLoginPrompt, LoginAttempt, LoginMode, LoginPrompt, LoginState, OAuthCredential, VersionedCredential, ProviderAdapter, ProviderId, ProviderLogin, ProviderLoginField, ProviderModel, ProviderModels, ProviderSummary, ProviderUsage, ProviderUsageContext, ProviderUsageData, Session, SubscriptionAccountDetails, UsageFact, UsageMeter, UsageResetCredit, UsageResetCredits, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createSubscriptionAuth, DEFAULT_ACCOUNT, SubscriptionAuth, } from "./auth.js";
|
|
2
2
|
export { MemoryCredentialStore } from "./memory-store.js";
|
|
3
|
-
export { parseChatGptUsage, parseCopilotUsage, parseGrokUsage } from "./usage.js";
|
|
3
|
+
export { mergeObservedUsage, parseChatGptUsage, parseChatGptUsageHeaders, parseCopilotUsage, parseCopilotUsageHeaders, parseGrokUsage, } from "./usage.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { parseChatGptUsage } from "../usage.js";
|
|
3
|
+
import { parseChatGptUsage, parseChatGptUsageHeaders } from "../usage.js";
|
|
4
4
|
import { abortableDelay, bearerRequest, isRecord, numberValue, requireAllowedHost, responseJson, stringArray, stringValue, } from "../utils.js";
|
|
5
5
|
const ISSUER = "https://auth.openai.com";
|
|
6
6
|
const DEFAULT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
@@ -533,6 +533,7 @@ export function chatGptProvider(options = {}) {
|
|
|
533
533
|
}
|
|
534
534
|
return response;
|
|
535
535
|
},
|
|
536
|
+
usageFromResponse: (response) => parseChatGptUsageHeaders(response.headers),
|
|
536
537
|
async getUsage({ fetch, signal }) {
|
|
537
538
|
const response = await fetch(USAGE_URL, { headers: { accept: "application/json" }, signal });
|
|
538
539
|
const raw = await responseJson(response, "ChatGPT usage");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copilotPlanName, parseCopilotUsage } from "../usage.js";
|
|
1
|
+
import { copilotPlanName, parseCopilotUsage, parseCopilotUsageHeaders } from "../usage.js";
|
|
2
2
|
import { abortableDelay, bearerRequest, isRecord, numberValue, requireAllowedHost, responseJson, stringArray, stringValue, } from "../utils.js";
|
|
3
3
|
const DEFAULT_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
4
4
|
const EXPIRY_SKEW_MS = 5 * 60_000;
|
|
@@ -448,6 +448,7 @@ export function copilotProvider(options = {}) {
|
|
|
448
448
|
headers,
|
|
449
449
|
});
|
|
450
450
|
},
|
|
451
|
+
usageFromResponse: (response) => parseCopilotUsageHeaders(response.headers),
|
|
451
452
|
async getUsage({ credential, signal }) {
|
|
452
453
|
if (!credential.refreshToken)
|
|
453
454
|
throw new Error("GitHub token is missing");
|
package/dist/types.d.ts
CHANGED
|
@@ -189,6 +189,11 @@ export interface ProviderAdapter {
|
|
|
189
189
|
refresh(credential: OAuthCredential, signal: AbortSignal): Promise<OAuthCredential>;
|
|
190
190
|
authorize(request: Request, credential: OAuthCredential): Request | Promise<Request>;
|
|
191
191
|
getUsage?(context: ProviderUsageContext): Promise<ProviderUsageData | null>;
|
|
192
|
+
/**
|
|
193
|
+
* Usage a model response reports about itself (rate-limit or quota headers), so meters stay
|
|
194
|
+
* current with no extra request. Only the meters the response mentions; `null` when none.
|
|
195
|
+
*/
|
|
196
|
+
usageFromResponse?(response: Response): ProviderUsageData | null;
|
|
192
197
|
getModels?(context: ProviderUsageContext): Promise<ProviderModel[]>;
|
|
193
198
|
isPermanentRefreshError?(error: unknown): boolean;
|
|
194
199
|
}
|
package/dist/usage.d.ts
CHANGED
|
@@ -3,3 +3,21 @@ export declare function parseChatGptUsage(raw: unknown, detailedResetCredits?: u
|
|
|
3
3
|
export declare function copilotPlanName(raw: unknown): string | undefined;
|
|
4
4
|
export declare function parseCopilotUsage(raw: unknown): ProviderUsageData | null;
|
|
5
5
|
export declare function parseGrokUsage(raw: unknown, userRaw?: unknown, settingsRaw?: unknown): ProviderUsageData | null;
|
|
6
|
+
/**
|
|
7
|
+
* The plan windows a ChatGPT (Codex) response reports in its headers, the same numbers the usage
|
|
8
|
+
* endpoint returns: `x-codex-primary-used-percent`, `-window-minutes` and `-reset-at` (unix
|
|
9
|
+
* seconds), likewise `secondary`, and other metered families as `x-<limit>-primary-…`. Meter ids
|
|
10
|
+
* match `parseChatGptUsage`, so a reading updates the meters a usage read listed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseChatGptUsageHeaders(headers: Headers): ProviderUsageData | null;
|
|
13
|
+
/**
|
|
14
|
+
* The quotas a GitHub Copilot response reports in `x-quota-snapshot-<quota>` headers, as VS Code
|
|
15
|
+
* reads them: URL-encoded `ent` (entitlement, -1 unlimited), `rem` (percent remaining) and `rst`
|
|
16
|
+
* (reset time). Meter ids match `parseCopilotUsage`'s quota snapshot keys.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseCopilotUsageHeaders(headers: Headers): ProviderUsageData | null;
|
|
19
|
+
/**
|
|
20
|
+
* `base` with the meters a response reported (`observed`) replaced by id. Meters `base` doesn't
|
|
21
|
+
* list are left out: a response reports numbers, the usage read decides which meters exist.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mergeObservedUsage<T extends ProviderUsageData>(base: T, observed: ProviderUsageData): T;
|
package/dist/usage.js
CHANGED
|
@@ -416,3 +416,85 @@ export function parseGrokUsage(raw, userRaw, settingsRaw) {
|
|
|
416
416
|
: undefined,
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* The plan windows a ChatGPT (Codex) response reports in its headers, the same numbers the usage
|
|
421
|
+
* endpoint returns: `x-codex-primary-used-percent`, `-window-minutes` and `-reset-at` (unix
|
|
422
|
+
* seconds), likewise `secondary`, and other metered families as `x-<limit>-primary-…`. Meter ids
|
|
423
|
+
* match `parseChatGptUsage`, so a reading updates the meters a usage read listed.
|
|
424
|
+
*/
|
|
425
|
+
export function parseChatGptUsageHeaders(headers) {
|
|
426
|
+
const families = new Set();
|
|
427
|
+
for (const name of headers.keys()) {
|
|
428
|
+
const match = /^x-(.+)-(?:primary|secondary)-used-percent$/.exec(name.toLowerCase());
|
|
429
|
+
if (match)
|
|
430
|
+
families.add(match[1]);
|
|
431
|
+
}
|
|
432
|
+
const meters = [];
|
|
433
|
+
for (const family of families) {
|
|
434
|
+
for (const window of ["primary", "secondary"]) {
|
|
435
|
+
const prefix = `x-${family}-${window}`;
|
|
436
|
+
const percentUsed = numeric(headers.get(`${prefix}-used-percent`));
|
|
437
|
+
if (percentUsed == null)
|
|
438
|
+
continue;
|
|
439
|
+
const minutes = numeric(headers.get(`${prefix}-window-minutes`));
|
|
440
|
+
const resetAt = timestamp(headers.get(`${prefix}-reset-at`));
|
|
441
|
+
const name = headers.get(`x-${family}-limit-name`)?.trim();
|
|
442
|
+
const label = windowLabel(minutes != null ? minutes * 60 : undefined);
|
|
443
|
+
meters.push({
|
|
444
|
+
id: family === "codex" ? window : `${family.replace(/-/g, "_")}:${window}`,
|
|
445
|
+
label: family === "codex" ? label : `${name || title(family)} / ${label}`,
|
|
446
|
+
unit: "percent",
|
|
447
|
+
percentUsed,
|
|
448
|
+
resetAt,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return meters.length ? { meters } : null;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* The quotas a GitHub Copilot response reports in `x-quota-snapshot-<quota>` headers, as VS Code
|
|
456
|
+
* reads them: URL-encoded `ent` (entitlement, -1 unlimited), `rem` (percent remaining) and `rst`
|
|
457
|
+
* (reset time). Meter ids match `parseCopilotUsage`'s quota snapshot keys.
|
|
458
|
+
*/
|
|
459
|
+
export function parseCopilotUsageHeaders(headers) {
|
|
460
|
+
const meters = [];
|
|
461
|
+
for (const [name, value] of headers) {
|
|
462
|
+
const match = /^x-quota-snapshot-(.+)$/.exec(name.toLowerCase());
|
|
463
|
+
if (!match)
|
|
464
|
+
continue;
|
|
465
|
+
const params = new URLSearchParams(value);
|
|
466
|
+
const entitlement = numeric(params.get("ent"));
|
|
467
|
+
const percentRemaining = numeric(params.get("rem"));
|
|
468
|
+
if (entitlement == null || percentRemaining == null)
|
|
469
|
+
continue;
|
|
470
|
+
const id = match[1];
|
|
471
|
+
const resetAt = timestamp(params.get("rst"));
|
|
472
|
+
meters.push(entitlement === -1
|
|
473
|
+
? copilotMeter(id, { unlimited: true }, resetAt)
|
|
474
|
+
: copilotMeter(id, {
|
|
475
|
+
entitlement,
|
|
476
|
+
quota_remaining: (entitlement * percentRemaining) / 100,
|
|
477
|
+
percent_remaining: percentRemaining,
|
|
478
|
+
}, resetAt));
|
|
479
|
+
}
|
|
480
|
+
return meters.length ? { meters } : null;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* `base` with the meters a response reported (`observed`) replaced by id. Meters `base` doesn't
|
|
484
|
+
* list are left out: a response reports numbers, the usage read decides which meters exist.
|
|
485
|
+
*/
|
|
486
|
+
export function mergeObservedUsage(base, observed) {
|
|
487
|
+
if (!base.meters?.length || !observed.meters?.length)
|
|
488
|
+
return base;
|
|
489
|
+
const byId = new Map(observed.meters.map((meter) => [meter.id, meter]));
|
|
490
|
+
let changed = false;
|
|
491
|
+
const meters = base.meters.map((meter) => {
|
|
492
|
+
const next = byId.get(meter.id);
|
|
493
|
+
if (!next)
|
|
494
|
+
return meter;
|
|
495
|
+
changed = true;
|
|
496
|
+
// The read's label and unit name the meter; the response only brings its numbers.
|
|
497
|
+
return { ...next, label: meter.label, unit: meter.unit, window: meter.window };
|
|
498
|
+
});
|
|
499
|
+
return changed ? { ...base, meters } : base;
|
|
500
|
+
}
|