@allixsenos/asu 0.3.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/LICENSE +21 -0
- package/README.md +497 -0
- package/dist/cache.d.ts +15 -0
- package/dist/cache.js +112 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +95 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/local.d.ts +14 -0
- package/dist/local.js +121 -0
- package/dist/local.js.map +1 -0
- package/dist/models.d.ts +184 -0
- package/dist/models.js +57 -0
- package/dist/models.js.map +1 -0
- package/dist/output.d.ts +13 -0
- package/dist/output.js +187 -0
- package/dist/output.js.map +1 -0
- package/dist/providers/base.d.ts +25 -0
- package/dist/providers/base.js +2 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/claude.d.ts +4 -0
- package/dist/providers/claude.js +167 -0
- package/dist/providers/claude.js.map +1 -0
- package/dist/providers/codex.d.ts +4 -0
- package/dist/providers/codex.js +66 -0
- package/dist/providers/codex.js.map +1 -0
- package/dist/providers/copilot.d.ts +4 -0
- package/dist/providers/copilot.js +64 -0
- package/dist/providers/copilot.js.map +1 -0
- package/dist/providers/cursor.d.ts +6 -0
- package/dist/providers/cursor.js +69 -0
- package/dist/providers/cursor.js.map +1 -0
- package/dist/providers/grok.d.ts +4 -0
- package/dist/providers/grok.js +59 -0
- package/dist/providers/grok.js.map +1 -0
- package/dist/providers/kimi.d.ts +4 -0
- package/dist/providers/kimi.js +49 -0
- package/dist/providers/kimi.js.map +1 -0
- package/dist/providers/minimax.d.ts +4 -0
- package/dist/providers/minimax.js +73 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/providers/parse.d.ts +16 -0
- package/dist/providers/parse.js +78 -0
- package/dist/providers/parse.js.map +1 -0
- package/dist/providers/zai.d.ts +5 -0
- package/dist/providers/zai.js +67 -0
- package/dist/providers/zai.js.map +1 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +45 -0
- package/dist/registry.js.map +1 -0
- package/dist/service.d.ts +28 -0
- package/dist/service.js +117 -0
- package/dist/service.js.map +1 -0
- package/dist/transport.d.ts +8 -0
- package/dist/transport.js +63 -0
- package/dist/transport.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/docs/architecture.md +83 -0
- package/docs/provider-contracts.md +32 -0
- package/docs/releasing.md +51 -0
- package/package.json +48 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { UsageCache, CACHE_TTL_MS, cacheKey } from './cache.js';
|
|
2
|
+
import { UsageError, safeReason } from './errors.js';
|
|
3
|
+
import { emptyUsage, providerUsageSchema, usageDataSchema } from './models.js';
|
|
4
|
+
import { createLocalContext } from './local.js';
|
|
5
|
+
import { createTransport } from './transport.js';
|
|
6
|
+
export async function deadline(work, timeoutMs) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
let timer;
|
|
9
|
+
const timeout = new Promise((_, reject) => {
|
|
10
|
+
timer = setTimeout(() => { controller.abort(); reject(new UsageError('timeout')); }, timeoutMs);
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
return await Promise.race([Promise.resolve().then(() => work(controller.signal)), timeout]);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function assertCredentials(credentials, now) {
|
|
20
|
+
if (!credentials || typeof credentials.token !== 'string' || !credentials.token.trim()
|
|
21
|
+
|| /[\r\n]/.test(credentials.token) || credentials.token.length > 65_536)
|
|
22
|
+
throw new UsageError('invalid_credentials');
|
|
23
|
+
if (credentials.expiresAt !== undefined && (!Number.isFinite(credentials.expiresAt) || credentials.expiresAt <= now))
|
|
24
|
+
throw new UsageError('invalid_credentials');
|
|
25
|
+
// Inspect expiry only, never refresh or infer identity from JWT claims.
|
|
26
|
+
const parts = credentials.token.split('.');
|
|
27
|
+
if (parts.length === 3) {
|
|
28
|
+
try {
|
|
29
|
+
const claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
30
|
+
if (claims && typeof claims === 'object' && 'exp' in claims && typeof claims.exp === 'number' && claims.exp * 1000 <= now)
|
|
31
|
+
throw new UsageError('invalid_credentials');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof UsageError)
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class UsageService {
|
|
40
|
+
providers;
|
|
41
|
+
local;
|
|
42
|
+
request;
|
|
43
|
+
cache;
|
|
44
|
+
now;
|
|
45
|
+
timeoutMs;
|
|
46
|
+
constructor(providers, options = {}) {
|
|
47
|
+
this.providers = providers;
|
|
48
|
+
this.local = options.local ?? createLocalContext();
|
|
49
|
+
this.request = options.request ?? createTransport();
|
|
50
|
+
this.now = options.now ?? Date.now;
|
|
51
|
+
this.cache = options.cache ?? new UsageCache(undefined, this.now);
|
|
52
|
+
this.timeoutMs = options.timeoutMs ?? 12_000;
|
|
53
|
+
}
|
|
54
|
+
async collect(options = {}) {
|
|
55
|
+
const selected = options.providerIds?.length ? this.providers.filter(p => options.providerIds.includes(p.id)) : this.providers;
|
|
56
|
+
const providers = await Promise.all(selected.map(provider => this.collectProvider(provider, options.fresh ?? false)));
|
|
57
|
+
return { schemaVersion: 1, generatedAt: new Date(this.now()).toISOString(), warnings: [...this.cache.warnings], providers };
|
|
58
|
+
}
|
|
59
|
+
result(provider, installed, credentialsPresent, error) {
|
|
60
|
+
const reason = error ? safeReason(error) : null;
|
|
61
|
+
const unavailable = reason && ['missing_credentials', 'invalid_credentials', 'credential_read_error', 'unauthorized'].includes(reason.code);
|
|
62
|
+
const now = this.now();
|
|
63
|
+
return { ...emptyUsage(), providerId: provider.id, displayName: provider.displayName,
|
|
64
|
+
experimental: provider.experimental ?? false, installed, credentialsPresent,
|
|
65
|
+
authenticated: reason ? unavailable ? false : null : true,
|
|
66
|
+
availability: reason ? unavailable ? 'unavailable' : 'error' : 'available', reason,
|
|
67
|
+
fetchedAt: new Date(now).toISOString(), expiresAt: new Date(now + CACHE_TTL_MS).toISOString(), cached: false };
|
|
68
|
+
}
|
|
69
|
+
async collectProvider(provider, fresh) {
|
|
70
|
+
let installed = null;
|
|
71
|
+
let credentials = null;
|
|
72
|
+
try {
|
|
73
|
+
// Re-read local credentials on every invocation so account/token changes bypass stale entries.
|
|
74
|
+
const discovery = await deadline(async () => Promise.allSettled([
|
|
75
|
+
provider.detect(this.local), provider.resolveCredentials(this.local),
|
|
76
|
+
]), this.timeoutMs);
|
|
77
|
+
installed = discovery[0].status === 'fulfilled' ? discovery[0].value : null;
|
|
78
|
+
if (discovery[1].status === 'rejected')
|
|
79
|
+
throw discovery[1].reason;
|
|
80
|
+
credentials = discovery[1].value;
|
|
81
|
+
if (!credentials)
|
|
82
|
+
return this.result(provider, installed, false, new UsageError('missing_credentials'));
|
|
83
|
+
assertCredentials(credentials, this.now());
|
|
84
|
+
const resolved = credentials;
|
|
85
|
+
const key = cacheKey([1, provider.id, provider.version, this.local.home,
|
|
86
|
+
resolved.token, resolved.accountId, resolved.metadata]);
|
|
87
|
+
const result = await this.cache.getOrFetch(key, async () => {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await deadline(signal => provider.fetchUsage({ ...this.local, request: this.request, signal, now: this.now }, resolved), this.timeoutMs);
|
|
90
|
+
const parsed = usageDataSchema.safeParse(raw);
|
|
91
|
+
if (!parsed.success)
|
|
92
|
+
throw new UsageError('invalid_response');
|
|
93
|
+
// Extra adapter properties are stripped before persistence; scrub credential echoes too.
|
|
94
|
+
const serialized = JSON.stringify(parsed.data, (_, value) => {
|
|
95
|
+
if (typeof value !== 'string')
|
|
96
|
+
return value;
|
|
97
|
+
for (const secret of [resolved.token, resolved.accountId]) {
|
|
98
|
+
if (secret)
|
|
99
|
+
value = value.replaceAll(secret, '[redacted]');
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
});
|
|
103
|
+
const data = usageDataSchema.parse(JSON.parse(serialized));
|
|
104
|
+
return providerUsageSchema.parse({ ...this.result(provider, installed, true), ...data });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return this.result(provider, installed, true, error);
|
|
108
|
+
}
|
|
109
|
+
}, fresh);
|
|
110
|
+
return { ...result, installed, credentialsPresent: true };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return this.result(provider, installed, credentials !== null, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface JsonRequest {
|
|
2
|
+
method?: 'GET' | 'POST';
|
|
3
|
+
headers?: Record<string, string>;
|
|
4
|
+
body?: unknown;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
export type RequestJson = (url: string, options?: JsonRequest) => Promise<unknown>;
|
|
8
|
+
export declare function createTransport(fetcher?: typeof fetch, timeoutMs?: number, maxBytes?: number): RequestJson;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { UsageError } from './errors.js';
|
|
2
|
+
import { version } from './version.js';
|
|
3
|
+
export function createTransport(fetcher = fetch, timeoutMs = 8_000, maxBytes = 1_048_576) {
|
|
4
|
+
return async (url, options = {}) => {
|
|
5
|
+
if (new URL(url).protocol !== 'https:')
|
|
6
|
+
throw new UsageError('provider_error');
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
|
+
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetcher(url, {
|
|
12
|
+
method: options.method ?? 'GET',
|
|
13
|
+
headers: { Accept: 'application/json', 'User-Agent': `asu/${version}`,
|
|
14
|
+
...(options.body === undefined ? {} : { 'Content-Type': 'application/json' }), ...options.headers },
|
|
15
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
16
|
+
redirect: 'error', signal,
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
await response.body?.cancel();
|
|
20
|
+
throw new UsageError(response.status === 401 || response.status === 403 ? 'unauthorized'
|
|
21
|
+
: response.status === 429 ? 'rate_limited' : 'http_error');
|
|
22
|
+
}
|
|
23
|
+
if (!response.body)
|
|
24
|
+
throw new UsageError('invalid_response');
|
|
25
|
+
const reader = response.body.getReader();
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let size = 0;
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
const { done, value } = await reader.read();
|
|
31
|
+
if (done)
|
|
32
|
+
break;
|
|
33
|
+
size += value.byteLength;
|
|
34
|
+
if (size > maxBytes) {
|
|
35
|
+
await reader.cancel();
|
|
36
|
+
throw new UsageError('invalid_response');
|
|
37
|
+
}
|
|
38
|
+
chunks.push(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
reader.releaseLock();
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new UsageError('invalid_response');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (signal.aborted)
|
|
53
|
+
throw new UsageError('timeout');
|
|
54
|
+
if (error instanceof UsageError)
|
|
55
|
+
throw error;
|
|
56
|
+
throw new UsageError('http_error');
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
/** The package version, read once from package.json so the CLI, User-Agent and package never disagree. */
|
|
3
|
+
export const version = createRequire(import.meta.url)('../package.json').version;
|
|
4
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,0GAA0G;AAC1G,MAAM,CAAC,MAAM,OAAO,GAAY,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAyB,CAAC,OAAO,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
CLI arguments
|
|
5
|
+
↓
|
|
6
|
+
Provider registry (built-ins + explicit trusted plugins)
|
|
7
|
+
↓
|
|
8
|
+
Usage service (parallel providers, isolated failures, deadlines)
|
|
9
|
+
↓ ↓
|
|
10
|
+
Read-only credential lookup Five-minute memory/disk cache + process locks
|
|
11
|
+
↓ ↓
|
|
12
|
+
Provider usage API → adapter normalization → schema validation + redaction
|
|
13
|
+
↓
|
|
14
|
+
Versioned report → plain text / table / JSON
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package exports a library API and the `asu` executable. It contains no frontend and no daemon. The provider ID and version identify an adapter. The credentials are part of the cache key, so two accounts never share a cached result by accident.
|
|
18
|
+
|
|
19
|
+
## Provider contract
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
interface Provider {
|
|
23
|
+
id: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
version: number;
|
|
26
|
+
experimental?: boolean;
|
|
27
|
+
detect(context: LocalContext): Promise<boolean | null>;
|
|
28
|
+
resolveCredentials(context: LocalContext): Promise<Credentials | null>;
|
|
29
|
+
fetchUsage(context: ProviderContext, credentials: Credentials): Promise<UsageData>;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
An adapter owns its credential formats, its HTTP request, and its response normalization. `LocalContext` supplies the home directory, the environment, the platform, bounded file readers, a Keychain lookup, and a read-only SQLite lookup. `ProviderContext` adds the JSON HTTP transport, an abort signal, and a clock. Consumers never receive the credentials object. A new provider needs no new branch in the usage service or in the output code.
|
|
34
|
+
|
|
35
|
+
`detect` examines whether the executable or app is present. It does not start the provider. A provider that users reach through other clients, such as Z.ai, can return `null`. `resolveCredentials` reads the credentials on each invocation. It must not refresh them. `fetchUsage` must pass `context.signal` to `context.request`.
|
|
36
|
+
|
|
37
|
+
The service runs the provider functions concurrently. Each result and each failure is independent. The service validates the normalized output, removes undeclared properties, removes terminal control characters, and redacts echoed tokens and account IDs before it caches the result. A plugin exception never appears in the output as written. Plugins are trusted local code and run without a sandbox. Do not load a module you do not trust. The asynchronous deadline cannot stop a synchronous plugin that blocks the Node event loop.
|
|
38
|
+
|
|
39
|
+
## External plugin example
|
|
40
|
+
|
|
41
|
+
Save an ESM module, for example `my-provider.mjs`, that exports `default` or `provider`:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
export default {
|
|
45
|
+
id: 'my-provider',
|
|
46
|
+
displayName: 'My provider',
|
|
47
|
+
version: 1,
|
|
48
|
+
experimental: true,
|
|
49
|
+
async detect() { return null; },
|
|
50
|
+
async resolveCredentials(context) {
|
|
51
|
+
const token = context.env.MY_PROVIDER_TOKEN;
|
|
52
|
+
return token ? { token } : null;
|
|
53
|
+
},
|
|
54
|
+
async fetchUsage(context, credentials) {
|
|
55
|
+
// Call the provider's fixed HTTPS usage endpoint through context.request,
|
|
56
|
+
// validate its response, and return only normalized UsageData.
|
|
57
|
+
return {
|
|
58
|
+
planLabel: null,
|
|
59
|
+
windows: [],
|
|
60
|
+
balances: [],
|
|
61
|
+
details: [{ label: 'Integration', value: 'Example adapter; no usage endpoint configured' }],
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node dist/cli.js --plugin ./my-provider.mjs --provider my-provider --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
ASU resolves an installed package name from the current working directory. It never downloads a plugin and never searches for one. It rejects a duplicate provider ID and a malformed interface. A TypeScript plugin can import `Provider`, `Credentials`, `UsageData`, and the other exported types from `@allixsenos/asu`. Distribute compiled JavaScript so that Node can load it. Bump `version` after you change the request or the normalization semantics of an adapter.
|
|
72
|
+
|
|
73
|
+
## Report contract
|
|
74
|
+
|
|
75
|
+
`schemaVersion: 1` is the machine interface. `generatedAt` describes the report. `fetchedAt` and `expiresAt` describe each provider snapshot. A result includes `providerId`, `displayName`, `experimental`, `installed`, `credentialsPresent`, `authenticated`, `availability`, an optional `reason`, `planLabel`, `windows`, `balances`, `details`, and `cached`.
|
|
76
|
+
|
|
77
|
+
A window has a stable ID, a label, `percentUsed`, and a UTC `resetsAt`. It can also have quantities, a unit, an unlimited flag, and a model or surface scope. An unknown percentage or reset is `null`. An absent balance or quantity is not zero. A percentage can exceed 100 when the provider reports overage. The human renderers round to two decimals. JSON keeps the normalized precision. An unlimited window shows no percentage.
|
|
78
|
+
|
|
79
|
+
Availability is one of three values. `available` means the provider returned usage. `unavailable` means the credentials are missing, rejected, or unreadable. `error` means the fetch or the normalization failed. `authenticated` is `true` only after a recognized successful usage response. It is `false` for unavailable credentials. It is `null` after a request failure that cannot establish authentication. These are snapshot values. They do not promise that the token stays valid after `fetchedAt`.
|
|
80
|
+
|
|
81
|
+
## Tests
|
|
82
|
+
|
|
83
|
+
Normalization tests cover observed and synthetic schemas. Credential tests inject environment variables and files, then examine precedence, expiration, and read-only behavior. Service tests use fake clocks and concurrent callers to examine isolation, coalescing, TTL expiry, account changes, persistent locks, corruption, and redaction. CLI tests run real subprocesses and parse stdout, including the symlinked entry points that npm creates. Live checks are a separate, documented validation step.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Provider contracts
|
|
2
|
+
|
|
3
|
+
We checked these contracts against the sources below on **2026-09-07**. Public source code and fixtures give implementation evidence, not a stable vendor guarantee. No adapter refreshes credentials. HTTP 401 and 403 make a provider unavailable. Other HTTP and schema failures stay isolated and sanitized.
|
|
4
|
+
|
|
5
|
+
The named reference is [Paseo's quota-fetcher](https://github.com/getpaseo/paseo/tree/main/packages/server/src/services/quota-fetcher/providers). ASU implements its own plugin and output contracts. Where a provider publishes no API documentation, the table says whether the evidence comes from the Paseo reference or from an official provider source.
|
|
6
|
+
|
|
7
|
+
| Provider | Request and headers | Normalization and evidence |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Claude | `GET https://api.anthropic.com/api/oauth/usage`. Bearer token, `anthropic-beta: oauth-2025-04-20`, JSON Accept. | `five_hour` and `seven_day` supply `utilization` and `resets_at`. Legacy `seven_day_*` keys and the `limits[]` and `model_scoped[]` weekly scopes are also read. [Provider issue that documents the scoped fields](https://github.com/anthropics/claude-code/issues/79022), [Paseo adapter](https://github.com/getpaseo/paseo/blob/main/packages/server/src/services/quota-fetcher/providers/claude.ts). Live check passed. |
|
|
10
|
+
| Codex | `GET https://chatgpt.com/backend-api/wham/usage`. Bearer token, optional `ChatGPT-Account-Id`. | Primary and secondary windows, the legacy code-review field, `additional_rate_limits`, `plan_type`, and credits. [Official generated response model](https://github.com/openai/codex/blob/main/codex-rs/codex-backend-openapi-models/src/models/rate_limit_status_payload.rs), [window model](https://github.com/openai/codex/blob/main/codex-rs/codex-backend-openapi-models/src/models/rate_limit_window_snapshot.rs). Live check passed. |
|
|
11
|
+
| Copilot | `GET https://api.github.com/copilot_internal/user`. `Authorization: token …`, JSON Accept, editor and plugin version headers copied from the Paseo request. | `copilot_plan`, `quota_reset_date`, and quantities only from `quota_snapshots`. The reset date becomes a detail only when the response has no quota windows, because each window carries it. | [Official quota parser](https://github.com/microsoft/vscode-copilot-chat/blob/main/src/platform/chat/common/chatQuotaServiceImpl.ts), [Paseo request](https://github.com/getpaseo/paseo/blob/main/packages/server/src/services/quota-fetcher/providers/copilot.ts). Live check passed. |
|
|
12
|
+
| Cursor | `POST https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage`. Body `{}`, Bearer token, JSON Content-Type, `Connect-Protocol-Version: 1`. | `planUsage.totalSpend`, `remaining`, and `limit` are cents. Billing-cycle timestamps can be numeric seconds, numeric milliseconds, or ISO strings. [Paseo adapter and credential formats](https://github.com/getpaseo/paseo/blob/main/packages/server/src/services/quota-fetcher/providers/cursor.ts). We found no public contract for an individual subscription. Experimental. |
|
|
13
|
+
| Z.ai | `GET https://api.z.ai/api/biz/subscription/list` with a Bearer token. Separately, `GET https://api.z.ai/api/monitor/usage/quota/limit` with the raw token in Authorization. | The plan list and the quota are separate requests. `TOKENS_LIMIT` and `TIME_LIMIT` carry percentages. A plan failure keeps the quota result, and a quota failure keeps the plan result. [Official quota-query script](https://github.com/zai-org/zai-coding-plugins/blob/main/plugins/glm-plan-usage/skills/usage-query-skill/scripts/query-usage.mjs), [Paseo plan endpoint](https://github.com/getpaseo/paseo/blob/main/packages/server/src/services/quota-fetcher/providers/zai.ts). Experimental. |
|
|
14
|
+
| Grok | `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits`. Bearer token, `X-XAI-Token-Auth: xai-grok-cli`. | `config.creditUsagePercent`, `currentPeriod`, the optional legacy `monthlyLimit` and `used`, and the prepaid balance. The official source defines `Cent.val` as USD cents, so ASU converts monetary balances to USD. [Official billing source](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-shell/src/extensions/billing.rs), [auth headers](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-shell/src/util/grok_auth_credentials.rs). Experimental. |
|
|
15
|
+
| Kimi | `GET https://api.kimi.com/coding/v1/usages`. Bearer token, JSON Accept. | `usage` is the weekly summary. `limits[].detail` supplies used, limit, and resetTime as numeric strings. `limits[].window` supplies duration and timeUnit. [Official parser and request](https://github.com/MoonshotAI/kimi-code/blob/main/packages/oauth/src/managed-usage.ts), [official legacy credential fixture](https://github.com/MoonshotAI/kimi-code/blob/main/packages/migration-legacy/test/fixtures/golden/.kimi/credentials/kimi-code.json). Experimental. |
|
|
16
|
+
| MiniMax | `GET /v1/token_plan/remains`. Bearer token, JSON Content-Type. Global host `api.minimax.io` or China host `api.minimaxi.com`. The `www` hosts are also accepted. | `model_remains[]` with interval and weekly percentages and reset timestamps. A remaining percentage takes precedence. The count fallback treats `usage_count` as used, as in the current Paseo adapter. [Official global endpoint example](https://platform.minimax.io/subscribe/token-plan), [official China endpoint example](https://platform.minimaxi.com/docs/token-plan/faq), [Paseo parser and `.mmx` credentials](https://github.com/getpaseo/paseo/blob/main/packages/server/src/services/quota-fetcher/providers/minimax.ts). Experimental. The numeric fallback needs a live check in each region. |
|
|
17
|
+
|
|
18
|
+
Claude's configured home follows the official [`CLAUDE_CONFIG_DIR` documentation](https://code.claude.com/docs/en/env-vars). `CLAUDE_HOME` stays as a compatibility fallback. The access token, the subscription type, and the rate-limit tier come from the local OAuth metadata. On macOS, ASU queries the `Claude Code-credentials` service for the current account first, then does the legacy lookup without an account. A failed lookup and a malformed account-specific entry both fall through to the legacy lookup.
|
|
19
|
+
|
|
20
|
+
Codex reads subscription OAuth credentials, not ordinary API keys. The [official authentication documentation](https://developers.openai.com/codex/auth/) describes its file and keyring choices. ASU reads the `auth.json` sources listed in the README. It does not start a login, and it does not read or refresh a session that lives only in the keyring.
|
|
21
|
+
|
|
22
|
+
## Known limitations
|
|
23
|
+
|
|
24
|
+
- Only Claude, Codex, and Copilot passed live checks, all on Linux. The other adapters have fixture coverage only. The Keychain and desktop-store paths for macOS and Windows have no live check.
|
|
25
|
+
- Copilot's internal endpoint can return plan-only or legacy quota data. It may not represent every newer AI-credit billing arrangement. A zero entitlement can come with a reported 100% usage value. ASU keeps that percentage and the reported zero quantities. It does not infer the number of consumed requests.
|
|
26
|
+
- Copilot credentials must come from the listed environment variables or from the GitHub CLI `hosts.yml`. A login through another CLI store does not guarantee that these sources exist.
|
|
27
|
+
- Claude validates each additive scoped entry on its own. ASU omits a malformed entry and adds a normalized warning. The valid top-level windows survive.
|
|
28
|
+
- Cursor's individual usage endpoint comes from the named reference, not from a documented vendor contract. An unknown plan limit stays unknown.
|
|
29
|
+
- Z.ai can return more than one subscription record. ASU keeps the labels and status details and does not guess which subscription is active. There is no China ZHIPU adapter.
|
|
30
|
+
- Kimi booster-wallet balances and Grok historical and on-demand spending are not normalized yet. Their subscription windows are available.
|
|
31
|
+
- MiniMax's regional response schemas and count semantics need a live check. ASU rejects an unrecognized host instead of sending credentials to an arbitrary resource URL.
|
|
32
|
+
- Tests and diagnostics never record a real credential file or a raw authenticated response. The repository contains no real-account fixture.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
[release-please](https://github.com/googleapis/release-please) drives releases from the Conventional Commit history. A release is a Git tag on `main`, a GitHub release with the packed tarball attached, and a version of [`@allixsenos/asu`](https://www.npmjs.com/package/@allixsenos/asu) on the public npm registry.
|
|
4
|
+
|
|
5
|
+
## How a release happens
|
|
6
|
+
|
|
7
|
+
1. Each push to `main` runs the release workflow.
|
|
8
|
+
2. release-please opens or updates a pull request named `chore(main): release <version>`. The PR bumps `package.json` and `package-lock.json` and writes the new `CHANGELOG.md` section.
|
|
9
|
+
3. Review the PR and squash merge it.
|
|
10
|
+
4. release-please then creates the tag `v<version>` and the GitHub release.
|
|
11
|
+
5. The `publish` job checks out the tag, runs the checks and tests, packs the tarball, smoke tests it, attaches it to the release, and runs `npm publish --access public` against the npm registry.
|
|
12
|
+
|
|
13
|
+
## npm trusted publishing
|
|
14
|
+
|
|
15
|
+
The workflow holds no npm token. npm trusts the `release.yml` workflow of this repository through GitHub's OIDC identity, and npm attaches provenance to each version. The job needs the `id-token: write` permission and npm 11.5.1 or newer, so the job installs the latest npm before it publishes.
|
|
16
|
+
|
|
17
|
+
npm reads the trusted publisher from the package settings, so the package must exist before the workflow can publish it. The first publish is a manual step by the owner:
|
|
18
|
+
|
|
19
|
+
1. Make sure the npm account owns the `@allixsenos` scope. The scope must equal the npm username, or the name of an npm organization the account created.
|
|
20
|
+
2. Run `npm login` on a machine with the repository checked out at the release tag.
|
|
21
|
+
3. Run `npm publish --access public` from the repository root. npm asks for the one-time 2FA code.
|
|
22
|
+
4. On npmjs.com, open the package, then Settings, then "Trusted Publisher". Choose GitHub Actions and enter the user `allixsenos`, the repository `asu`, and the workflow filename `release.yml`. Leave the environment empty.
|
|
23
|
+
5. Optional: in "Publishing access", choose "Require two-factor authentication and disallow tokens". Trusted publishing keeps working, and a leaked token can no longer publish.
|
|
24
|
+
|
|
25
|
+
After that, every release publishes on its own.
|
|
26
|
+
|
|
27
|
+
## Version rules
|
|
28
|
+
|
|
29
|
+
- Conventional Commit types decide the bump: `feat` is minor, `fix` and `perf` are patch, a `!` or a `BREAKING CHANGE:` footer is major.
|
|
30
|
+
- Before 1.0.0, a breaking change bumps the minor version instead of the major version. This is the `bump-minor-pre-major` configuration. A feature still bumps the minor version.
|
|
31
|
+
- To force a version, add `Release-As: <version>` as a footer on a commit to `main`.
|
|
32
|
+
- A change to the JSON report that removes or renames a field must bump `schemaVersion` in `src/models.ts`. Additive fields do not.
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
- `release-please-config.json` holds the release type and the bump rules. `initial-version` sets the first release to 0.1.0, because release-please defaults to 1.0.0 when no release exists.
|
|
37
|
+
- The repository setting "Allow GitHub Actions to create and approve pull requests" must stay on. Without it, release-please cannot open the release PR.
|
|
38
|
+
- `dist/` is committed and reads the version from `package.json` at run time, so a release PR needs no rebuild.
|
|
39
|
+
- `.release-please-manifest.json` holds the last released version. release-please updates it in each release PR. Do not edit it by hand after the first release.
|
|
40
|
+
- The workflow uses the `RELEASE_PLEASE_TOKEN` secret when it exists, and the default `GITHUB_TOKEN` otherwise. Pull requests that the default token opens do not trigger CI, so the release PR shows no checks. A fine-grained PAT with contents and pull requests write access solves that.
|
|
41
|
+
- Tags that release-please creates are not signed with a personal key. GitHub creates them through the API.
|
|
42
|
+
|
|
43
|
+
## Install a released version
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx --yes @allixsenos/asu --table
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
No token is needed. The package is public on the npm registry.
|
|
50
|
+
|
|
51
|
+
Each GitHub release also carries the tarball twice: as `allixsenos-asu-<version>.tgz` and as `asu.tgz`. The second name gives `https://github.com/allixsenos/asu/releases/latest/download/asu.tgz` a stable URL for the newest release. Both work with `npx --yes <url>` and with `npm install <url>`, and neither needs a token or a build step.
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@allixsenos/asu",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Agent subscription usage for humans and agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"asu": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"docs"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.13.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/allixsenos/asu.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"registry": "https://registry.npmjs.org",
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"compile": "tsc -p tsconfig.json",
|
|
34
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"test": "npm run compile && node --test test/*.test.mjs",
|
|
36
|
+
"start": "node dist/cli.js"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"proper-lockfile": "^4.1.2",
|
|
40
|
+
"yaml": "^2.9.0",
|
|
41
|
+
"zod": "^4.3.6"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^24.13.3",
|
|
45
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
46
|
+
"typescript": "~5.9.3"
|
|
47
|
+
}
|
|
48
|
+
}
|