@byokit/usage 0.1.0 → 0.2.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/CHANGELOG.md +10 -0
- package/README.md +198 -11
- package/dist/backoff.d.ts +7 -0
- package/dist/backoff.js +26 -0
- package/dist/calls.d.ts +68 -0
- package/dist/calls.js +131 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +154 -50
- package/dist/ledger.d.ts +49 -0
- package/dist/ledger.js +87 -0
- package/dist/providers.d.ts +23 -4
- package/dist/providers.js +91 -13
- package/dist/quota.d.ts +7 -0
- package/dist/quota.js +87 -0
- package/dist/room.d.ts +3 -0
- package/dist/room.js +11 -0
- package/dist/store.d.ts +10 -9
- package/dist/store.js +31 -7
- package/dist/types.d.ts +71 -8
- package/dist/windows.d.ts +2 -1
- package/dist/windows.js +5 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,69 +1,153 @@
|
|
|
1
|
+
import { backoffDelayMs } from "./backoff.js";
|
|
1
2
|
import { accessSync, constants, statSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
2
4
|
import { isAbsolute, join } from 'node:path';
|
|
3
|
-
import { codexUsage, providerGet } from "./providers.js";
|
|
4
|
-
import { fingerprint, readJson, store } from "./store.js";
|
|
5
|
-
import { codexWindows, goWindows, record, zaiWindows } from "./windows.js";
|
|
5
|
+
import { claudeAuth, claudeUsage, codexUsage, customClaude, providerGet } from "./providers.js";
|
|
6
|
+
import { fingerprint, readJson, store, memoryUsageStore, safeWindows } from "./store.js";
|
|
7
|
+
import { claudeWindows, codexWindows, goWindows, record, zaiWindows } from "./windows.js";
|
|
8
|
+
import { codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from "./quota.js";
|
|
6
9
|
import { UsageError } from "./types.js";
|
|
7
10
|
export * from "./types.js";
|
|
11
|
+
export { callLedger, normalizeTokens, priceCall } from "./calls.js";
|
|
12
|
+
export { tokenLedger, memoryTokenLedgerStore, TokenLedgerError } from "./ledger.js";
|
|
13
|
+
export { roomOf } from "./room.js";
|
|
14
|
+
export { fingerprint, store as fileUsageStore, memoryUsageStore } from "./store.js";
|
|
15
|
+
export { retryAfterMs, backoffDelayMs, memoryBackoffPolicy } from "./backoff.js";
|
|
8
16
|
export { claudeWindows, codexWindows, goWindows, zaiWindows } from "./windows.js";
|
|
17
|
+
export { codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from "./quota.js";
|
|
9
18
|
export { WORDS, words, usageWords } from "./words.js";
|
|
10
|
-
const
|
|
19
|
+
const providers = ['claude', 'codex', 'opencode', 'zai', 'copilot', 'grok', 'minimax', 'gemini', 'kimi'];
|
|
20
|
+
const validText = (v) => typeof v === 'string' && !v.includes('\0') && !/[\r\n]/.test(v) && v.length <= 16384;
|
|
11
21
|
function validate(source) {
|
|
12
|
-
if (!record(source))
|
|
22
|
+
if (!record(source) || !providers.includes(source.provider))
|
|
13
23
|
throw new UsageError();
|
|
14
|
-
if (
|
|
15
|
-
if (
|
|
24
|
+
if ('credentialsFile' in source) {
|
|
25
|
+
if (source.provider !== 'claude' || ![source.credentialsFile, ...[source.configFile, source.statuslineFile].filter((v) => v !== undefined)].every((v) => validText(v) && isAbsolute(v)))
|
|
26
|
+
throw new UsageError();
|
|
27
|
+
}
|
|
28
|
+
else if ('read' in source) {
|
|
29
|
+
if (source.provider !== 'claude' || typeof source.read !== 'function' || !validText(source.accountUuid) || !source.accountUuid || source.connected !== undefined && typeof source.connected !== 'function')
|
|
30
|
+
throw new UsageError();
|
|
31
|
+
}
|
|
32
|
+
else if ('bin' in source) {
|
|
33
|
+
if (source.provider !== 'codex' || !validText(source.bin) || !isAbsolute(source.bin) || !validText(source.home) || !isAbsolute(source.home))
|
|
16
34
|
throw new UsageError();
|
|
17
35
|
if (source.env !== undefined && (!record(source.env) || Object.entries(source.env).some(([k, v]) => !k || k.includes('=') || !validText(k) || !validText(v))))
|
|
18
36
|
throw new UsageError();
|
|
19
37
|
}
|
|
20
|
-
else
|
|
21
|
-
|
|
38
|
+
else {
|
|
39
|
+
if ('key' in source && !['opencode', 'zai'].includes(source.provider) || 'access' in source && ['opencode', 'zai'].includes(source.provider))
|
|
40
|
+
throw new UsageError();
|
|
41
|
+
const credential = 'access' in source ? source.access : 'key' in source ? source.key : undefined;
|
|
42
|
+
if (!validText(credential))
|
|
43
|
+
throw new UsageError();
|
|
44
|
+
if ('access' in source && source.provider === 'codex' && (!validText(source.accountId) || !source.accountId))
|
|
45
|
+
throw new UsageError();
|
|
46
|
+
}
|
|
47
|
+
const fields = source;
|
|
48
|
+
for (const field of ['accountId', 'accountUuid', 'project'])
|
|
49
|
+
if (fields[field] !== undefined && (!validText(fields[field]) || !fields[field]))
|
|
50
|
+
throw new UsageError();
|
|
51
|
+
}
|
|
52
|
+
/** Claims provide a cache identity only; this does not authenticate or trust a token. */
|
|
53
|
+
function subject(access) {
|
|
54
|
+
try {
|
|
55
|
+
const encoded = access.split('.')[1];
|
|
56
|
+
if (!encoded)
|
|
57
|
+
return undefined;
|
|
58
|
+
const claims = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
59
|
+
return record(claims) && validText(claims.sub) && claims.sub ? claims.sub : undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
22
64
|
}
|
|
23
65
|
/** One reader owns per-account backoff and concurrent-read deduplication. */
|
|
24
66
|
export function usage(options) {
|
|
25
|
-
if (!validText(options.stateDir) || !isAbsolute(options.stateDir))
|
|
67
|
+
if (options.stateDir !== undefined && (!validText(options.stateDir) || !isAbsolute(options.stateDir)))
|
|
26
68
|
throw new UsageError();
|
|
27
|
-
const disk = store(options.stateDir);
|
|
69
|
+
const disk = options.store ?? (options.stateDir ? store(options.stateDir) : memoryUsageStore());
|
|
70
|
+
const transient = memoryUsageStore();
|
|
28
71
|
const fp = fingerprint(options.salt ?? 'byokit/usage/account');
|
|
29
|
-
const
|
|
72
|
+
const backoffs = new Map();
|
|
30
73
|
const inFlight = new Map();
|
|
31
74
|
const attempts = new Map();
|
|
32
75
|
const now = (opts) => opts?.nowMs ?? (options.now ?? Date.now)();
|
|
33
76
|
function connected(source) {
|
|
34
77
|
validate(source);
|
|
35
|
-
if (
|
|
36
|
-
return source
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
78
|
+
if ('credentialsFile' in source)
|
|
79
|
+
return claudeAuth(source) !== undefined;
|
|
80
|
+
if ('read' in source) {
|
|
81
|
+
try {
|
|
82
|
+
return source.connected?.() ?? true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
40
87
|
}
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
if ('bin' in source) {
|
|
89
|
+
try {
|
|
90
|
+
accessSync(source.bin, constants.X_OK);
|
|
91
|
+
return statSync(source.bin).isFile();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
43
96
|
}
|
|
97
|
+
return ('access' in source ? source.access : source.key).trim() !== '';
|
|
44
98
|
}
|
|
45
99
|
function account(source) {
|
|
46
100
|
validate(source);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
101
|
+
let id;
|
|
102
|
+
if ('credentialsFile' in source)
|
|
103
|
+
id = claudeAuth(source)?.account;
|
|
104
|
+
else if ('read' in source)
|
|
105
|
+
id = source.accountUuid;
|
|
106
|
+
else if ('bin' in source) {
|
|
107
|
+
const raw = readJson(join(source.home, 'auth.json'), 64 * 1024);
|
|
108
|
+
const tokens = record(raw) && record(raw.tokens) ? raw.tokens : undefined;
|
|
109
|
+
id = typeof tokens?.account_id === 'string' ? tokens.account_id : `codex-home\0${source.home}`;
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
id = source.accountId ?? (source.provider === 'claude' ? source.accountUuid : undefined) ?? ('access' in source ? subject(source.access) : undefined);
|
|
113
|
+
return id && id.length <= 16384 ? fp(source.provider, id) : undefined;
|
|
114
|
+
}
|
|
115
|
+
function identity(source) {
|
|
116
|
+
const stable = account(source);
|
|
117
|
+
if (stable)
|
|
118
|
+
return { key: stable, stable: true };
|
|
119
|
+
// Opaque credentials without an identity can be read, but never reach an app/disk store.
|
|
120
|
+
const credential = 'access' in source ? source.access : 'key' in source ? source.key : '';
|
|
121
|
+
return { key: createHash('sha256').update(`${source.provider}\0${credential}`).digest('hex'), stable: false };
|
|
122
|
+
}
|
|
123
|
+
function windows(source, raw, clock) {
|
|
124
|
+
switch (source.provider) {
|
|
125
|
+
case 'claude': return claudeWindows(raw);
|
|
126
|
+
case 'codex': return 'bin' in source ? codexWindows(raw) : codexTokenWindows(raw);
|
|
127
|
+
case 'opencode': return goWindows(record(raw) ? raw.usage : undefined);
|
|
128
|
+
case 'zai': return zaiWindows(record(raw) && record(raw.data) ? raw.data.limits : undefined);
|
|
129
|
+
case 'copilot': return copilotWindows(raw);
|
|
130
|
+
case 'grok': return grokWindows(raw);
|
|
131
|
+
case 'minimax': return minimaxWindows(raw);
|
|
132
|
+
case 'gemini': return geminiWindows(raw);
|
|
133
|
+
case 'kimi': return kimiWindows(raw, clock);
|
|
134
|
+
}
|
|
53
135
|
}
|
|
54
|
-
const windows = (source, raw) => source.provider === 'codex' ? codexWindows(raw) : source.provider === 'opencode' ? goWindows(raw) : zaiWindows(raw);
|
|
55
136
|
function lastKnown(source, opts) {
|
|
56
137
|
if (!connected(source))
|
|
57
138
|
return undefined;
|
|
58
|
-
const
|
|
59
|
-
if (!key)
|
|
60
|
-
return undefined;
|
|
61
|
-
const stored = disk.get(source.provider, key);
|
|
139
|
+
const id = identity(source);
|
|
62
140
|
const clock = now(opts);
|
|
63
|
-
|
|
141
|
+
try {
|
|
142
|
+
const stored = (id.stable ? disk : transient).get(source.provider, id.key);
|
|
143
|
+
if (!stored || !Number.isFinite(stored.at) || clock < stored.at || clock - stored.at > 86_400_000)
|
|
144
|
+
return undefined;
|
|
145
|
+
const rows = safeWindows(source.provider, stored.windows);
|
|
146
|
+
return rows.length ? { provider: source.provider, windows: rows, at: stored.at } : undefined;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
64
149
|
return undefined;
|
|
65
|
-
|
|
66
|
-
return rows.length ? { provider: source.provider, windows: rows, at: stored.at } : undefined;
|
|
150
|
+
}
|
|
67
151
|
}
|
|
68
152
|
async function read(source, opts) {
|
|
69
153
|
validate(source);
|
|
@@ -71,48 +155,68 @@ export function usage(options) {
|
|
|
71
155
|
const empty = (code) => ({ provider: source.provider, windows: [], at: clock, code });
|
|
72
156
|
if (!connected(source))
|
|
73
157
|
return empty('not-connected');
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
return empty('not-connected');
|
|
77
|
-
const identity = `${source.provider}\0${key}`;
|
|
158
|
+
const id = identity(source);
|
|
159
|
+
const key = `${source.provider}\0${id.key}`;
|
|
78
160
|
const previous = lastKnown(source, { nowMs: clock });
|
|
79
161
|
if (previous && clock - previous.at < 60_000)
|
|
80
162
|
return previous;
|
|
81
|
-
|
|
163
|
+
let until = backoffs.get(key) ?? 0;
|
|
164
|
+
try {
|
|
165
|
+
if (id.stable)
|
|
166
|
+
until = Math.max(until, options.backoff?.get(source.provider, id.key) ?? 0);
|
|
167
|
+
}
|
|
168
|
+
catch { /* keep internal backoff */ }
|
|
169
|
+
if (until > clock)
|
|
82
170
|
return previous ? { ...previous, code: 'rate-limited' } : empty('rate-limited');
|
|
83
|
-
const pending = inFlight.get(
|
|
171
|
+
const pending = inFlight.get(key);
|
|
84
172
|
if (pending)
|
|
85
173
|
return pending;
|
|
86
|
-
const attempt = attempts.get(
|
|
174
|
+
const attempt = attempts.get(key);
|
|
87
175
|
if (attempt && clock >= attempt.at && clock - attempt.at < 60_000)
|
|
88
|
-
return previous ? { ...previous, code: attempt.
|
|
176
|
+
return previous ? { ...previous, code: attempt.code } : empty(attempt.code ?? 'unavailable');
|
|
89
177
|
const task = (async () => {
|
|
90
178
|
let answer;
|
|
91
179
|
try {
|
|
92
|
-
answer =
|
|
180
|
+
answer = 'read' in source ? await customClaude(source, clock) : 'credentialsFile' in source ? await claudeUsage(source, options.fetch ?? globalThis.fetch, clock)
|
|
181
|
+
: 'bin' in source ? await codexUsage(source) : await providerGet(source, options.fetch ?? globalThis.fetch, clock);
|
|
93
182
|
}
|
|
94
183
|
catch {
|
|
95
184
|
answer = { code: 'unavailable' };
|
|
96
185
|
}
|
|
97
|
-
if (answer.
|
|
98
|
-
|
|
99
|
-
|
|
186
|
+
if (answer.code === 'rate-limited') {
|
|
187
|
+
let delay = backoffDelayMs(answer.retryAfterMs);
|
|
188
|
+
try {
|
|
189
|
+
const selected = options.backoff?.delayMs?.(answer.retryAfterMs);
|
|
190
|
+
if (selected !== undefined && Number.isFinite(selected) && selected >= 0)
|
|
191
|
+
delay = selected;
|
|
192
|
+
}
|
|
193
|
+
catch { /* default */ }
|
|
194
|
+
backoffs.set(key, clock + delay);
|
|
195
|
+
try {
|
|
196
|
+
if (id.stable)
|
|
197
|
+
options.backoff?.set(source.provider, id.key, clock + delay);
|
|
198
|
+
}
|
|
199
|
+
catch { /* internal backoff stands */ }
|
|
200
|
+
}
|
|
201
|
+
const rows = safeWindows(source.provider, windows(source, answer.raw, clock));
|
|
100
202
|
if (!answer.code && !rows.length)
|
|
101
203
|
answer = { code: 'incomplete' };
|
|
102
|
-
|
|
103
|
-
attempts.set(identity, { at: clock, answer: { code: answer.code } });
|
|
204
|
+
attempts.set(key, { at: clock, code: answer.code });
|
|
104
205
|
if (rows.length && !answer.code) {
|
|
105
|
-
|
|
206
|
+
try {
|
|
207
|
+
(id.stable ? disk : transient).put(source.provider, id.key, { at: clock, windows: rows });
|
|
208
|
+
}
|
|
209
|
+
catch { /* reads survive a store failure */ }
|
|
106
210
|
return { provider: source.provider, windows: rows, at: clock };
|
|
107
211
|
}
|
|
108
212
|
return previous ? { ...previous, code: answer.code } : empty(answer.code ?? 'unavailable');
|
|
109
213
|
})();
|
|
110
|
-
inFlight.set(
|
|
214
|
+
inFlight.set(key, task);
|
|
111
215
|
try {
|
|
112
216
|
return await task;
|
|
113
217
|
}
|
|
114
218
|
finally {
|
|
115
|
-
inFlight.delete(
|
|
219
|
+
inFlight.delete(key);
|
|
116
220
|
}
|
|
117
221
|
}
|
|
118
222
|
return { read, lastKnown, connected, account };
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CallRecord } from './calls.ts';
|
|
2
|
+
/** Counts measured tokens for a host's member; no credential, account or network access. */
|
|
3
|
+
export interface TokenEntry {
|
|
4
|
+
tokens: number;
|
|
5
|
+
time: number;
|
|
6
|
+
call?: CallRecord;
|
|
7
|
+
}
|
|
8
|
+
export interface TokenLedgerStore {
|
|
9
|
+
record(member: string, entry: TokenEntry): void;
|
|
10
|
+
/** Entries in [from, to); the ledger checks bounds again before counting. */
|
|
11
|
+
query(member: string, from: number, to: number): readonly TokenEntry[];
|
|
12
|
+
}
|
|
13
|
+
export interface TokenLedgerOptions {
|
|
14
|
+
store?: TokenLedgerStore;
|
|
15
|
+
/** Seven-day token allowance. Missing means uncapped; zero means no allowance. */
|
|
16
|
+
cap?: number | ((member: string) => number | undefined);
|
|
17
|
+
}
|
|
18
|
+
export interface TokenQuery {
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
tokens: number;
|
|
22
|
+
/** Present when call records lack reported total tokens; tokens is the known subtotal. */
|
|
23
|
+
unknownCalls?: number;
|
|
24
|
+
days: {
|
|
25
|
+
date: string;
|
|
26
|
+
tokens: number;
|
|
27
|
+
unknownCalls?: number;
|
|
28
|
+
}[];
|
|
29
|
+
week: {
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
tokens: number;
|
|
33
|
+
cap?: number;
|
|
34
|
+
remaining?: number;
|
|
35
|
+
unknownCalls?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface TokenLedger {
|
|
39
|
+
record(member: string, tokens: number, time: number): void;
|
|
40
|
+
query(member: string, from: number, to: number): TokenQuery;
|
|
41
|
+
}
|
|
42
|
+
export declare class TokenLedgerError extends Error {
|
|
43
|
+
readonly code: 'invalid' | 'store';
|
|
44
|
+
name: string;
|
|
45
|
+
constructor(code: 'invalid' | 'store');
|
|
46
|
+
}
|
|
47
|
+
export declare function memoryTokenLedgerStore(): TokenLedgerStore;
|
|
48
|
+
/** Local calendar days, including DST changes; all API timestamps are epoch milliseconds. */
|
|
49
|
+
export declare function tokenLedger(options?: TokenLedgerOptions): TokenLedger;
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export class TokenLedgerError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
name = 'TokenLedgerError';
|
|
4
|
+
constructor(code) { super(code === 'invalid' ? 'The app supplied an invalid token ledger value.' : 'The token ledger store did not answer.'); this.code = code; }
|
|
5
|
+
}
|
|
6
|
+
const validMember = (member) => typeof member === 'string' && !!member && member.length <= 1024 && !member.includes('\0');
|
|
7
|
+
const validTime = (time) => typeof time === 'number' && Number.isFinite(time) && !Number.isNaN(new Date(time).getTime());
|
|
8
|
+
const validCount = (tokens) => Number.isSafeInteger(tokens) && tokens >= 0;
|
|
9
|
+
const dateOf = (time) => {
|
|
10
|
+
const date = new Date(time);
|
|
11
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
12
|
+
};
|
|
13
|
+
export function memoryTokenLedgerStore() {
|
|
14
|
+
const entries = new Map();
|
|
15
|
+
return {
|
|
16
|
+
record: (member, entry) => { const rows = entries.get(member) ?? []; rows.push(copyEntry(entry)); entries.set(member, rows); },
|
|
17
|
+
query: (member, from, to) => (entries.get(member) ?? []).filter((entry) => entry.time >= from && entry.time < to).map(copyEntry),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Local calendar days, including DST changes; all API timestamps are epoch milliseconds. */
|
|
21
|
+
export function tokenLedger(options = {}) {
|
|
22
|
+
const store = options.store ?? memoryTokenLedgerStore();
|
|
23
|
+
if (options.cap !== undefined && typeof options.cap !== 'function' && !validCount(options.cap))
|
|
24
|
+
throw new TokenLedgerError('invalid');
|
|
25
|
+
return {
|
|
26
|
+
record(member, tokens, time) {
|
|
27
|
+
if (!validMember(member) || !validCount(tokens) || !validTime(time))
|
|
28
|
+
throw new TokenLedgerError('invalid');
|
|
29
|
+
try {
|
|
30
|
+
store.record(member, { tokens, time });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new TokenLedgerError('store');
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
query(member, from, to) {
|
|
37
|
+
if (!validMember(member) || !validTime(from) || !validTime(to) || to < from)
|
|
38
|
+
throw new TokenLedgerError('invalid');
|
|
39
|
+
const start = new Date(to);
|
|
40
|
+
start.setDate(start.getDate() - 7);
|
|
41
|
+
const weekFrom = start.getTime();
|
|
42
|
+
if (!validTime(weekFrom))
|
|
43
|
+
throw new TokenLedgerError('invalid');
|
|
44
|
+
let entries;
|
|
45
|
+
let cap;
|
|
46
|
+
try {
|
|
47
|
+
entries = store.query(member, Math.min(from, weekFrom), to);
|
|
48
|
+
cap = typeof options.cap === 'function' ? options.cap(member) : options.cap;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new TokenLedgerError('store');
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(entries) || cap !== undefined && !validCount(cap))
|
|
54
|
+
throw new TokenLedgerError('invalid');
|
|
55
|
+
let tokens = 0;
|
|
56
|
+
let weekTokens = 0;
|
|
57
|
+
let unknownCalls = 0;
|
|
58
|
+
let weekUnknown = 0;
|
|
59
|
+
const days = new Map();
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry || !validCount(entry.tokens) || !validTime(entry.time) || entry.time >= to)
|
|
62
|
+
continue;
|
|
63
|
+
const unknown = entry.call !== undefined && entry.call.tokens.total === undefined ? 1 : 0;
|
|
64
|
+
if (entry.time >= weekFrom) {
|
|
65
|
+
weekTokens += entry.tokens;
|
|
66
|
+
weekUnknown += unknown;
|
|
67
|
+
}
|
|
68
|
+
if (entry.time < from)
|
|
69
|
+
continue;
|
|
70
|
+
tokens += entry.tokens;
|
|
71
|
+
unknownCalls += unknown;
|
|
72
|
+
const date = dateOf(entry.time);
|
|
73
|
+
const previous = days.get(date) ?? { tokens: 0, unknownCalls: 0 };
|
|
74
|
+
days.set(date, { tokens: previous.tokens + entry.tokens, unknownCalls: previous.unknownCalls + unknown });
|
|
75
|
+
}
|
|
76
|
+
if (!Number.isSafeInteger(tokens) || !Number.isSafeInteger(weekTokens))
|
|
77
|
+
throw new TokenLedgerError('invalid');
|
|
78
|
+
return { from, to, tokens, ...(unknownCalls ? { unknownCalls } : {}), days: [...days].sort(([a], [b]) => a.localeCompare(b)).map(([date, counts]) => ({ date, tokens: counts.tokens, ...(counts.unknownCalls ? { unknownCalls: counts.unknownCalls } : {}) })),
|
|
79
|
+
week: { from: weekFrom, to, tokens: weekTokens, ...(weekUnknown ? { unknownCalls: weekUnknown } : {}), ...(cap === undefined ? {} : { cap, ...(weekUnknown ? {} : { remaining: Math.max(0, cap - weekTokens) }) }) } };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function copyEntry(entry) {
|
|
84
|
+
const call = entry.call;
|
|
85
|
+
return { tokens: entry.tokens, time: entry.time, ...(call ? { call: { ...call, tokens: { ...call.tokens },
|
|
86
|
+
...(call.cost ? { cost: { ...call.cost } } : {}), ...(call.limits ? { limits: call.limits.map((window) => ({ ...window })) } : {}) } } : {}) };
|
|
87
|
+
}
|
package/dist/providers.d.ts
CHANGED
|
@@ -2,11 +2,30 @@ import type { Code, Source } from './types.ts';
|
|
|
2
2
|
export interface Answer {
|
|
3
3
|
raw?: unknown;
|
|
4
4
|
code?: Code;
|
|
5
|
-
|
|
5
|
+
retryAfterMs?: number;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
type TokenSource = Extract<Source, {
|
|
8
|
+
access: string;
|
|
9
|
+
} | {
|
|
8
10
|
key: string;
|
|
9
|
-
}
|
|
11
|
+
}>;
|
|
12
|
+
export declare function providerGet(source: TokenSource, fetcher: typeof fetch, nowMs: number): Promise<Answer>;
|
|
13
|
+
/** A host hook gets the same deadline and failure envelope as built-in sources. */
|
|
14
|
+
export declare function customClaude(source: Extract<Source, {
|
|
15
|
+
read: unknown;
|
|
16
|
+
}>, nowMs: number): Promise<Answer>;
|
|
10
17
|
export declare function codexUsage(source: Extract<Source, {
|
|
11
|
-
|
|
18
|
+
bin: string;
|
|
12
19
|
}>): Promise<Answer>;
|
|
20
|
+
/** Re-read the tool's credentials so its own renewal is picked up; never renew or write them. */
|
|
21
|
+
export declare function claudeAuth(source: Extract<Source, {
|
|
22
|
+
credentialsFile: string;
|
|
23
|
+
}>): {
|
|
24
|
+
token: string;
|
|
25
|
+
expiresAt?: number;
|
|
26
|
+
account: string;
|
|
27
|
+
} | undefined;
|
|
28
|
+
export declare function claudeUsage(source: Extract<Source, {
|
|
29
|
+
credentialsFile: string;
|
|
30
|
+
}>, fetcher: typeof fetch, nowMs: number): Promise<Answer>;
|
|
31
|
+
export {};
|
package/dist/providers.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import { retryAfterMs } from "./backoff.js";
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import { readJson, readJsonSnapshot } from "./store.js";
|
|
4
|
+
import { claudeWindows, record } from "./windows.js";
|
|
5
|
+
import { grokWindows } from "./quota.js";
|
|
6
|
+
const USER_AGENT = 'byokit/usage/0.2.0';
|
|
7
|
+
/** One bounded request; credentials and response bodies never become errors. */
|
|
8
|
+
async function request(url, key, fetcher, nowMs, extra = {}) {
|
|
4
9
|
const controller = new AbortController();
|
|
5
10
|
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
6
11
|
try {
|
|
7
|
-
const
|
|
8
|
-
|
|
12
|
+
const response = await fetcher(url, { headers: { accept: 'application/json', authorization: `Bearer ${key}`, 'User-Agent': USER_AGENT,
|
|
13
|
+
...(extra.body !== undefined ? { 'content-type': 'application/json' } : {}), ...extra.headers },
|
|
14
|
+
...(extra.body !== undefined ? { method: 'POST', body: JSON.stringify(extra.body) } : {}), redirect: 'error', signal: controller.signal });
|
|
9
15
|
if (response.status === 429) {
|
|
10
|
-
const
|
|
11
|
-
const seconds = Number(retry);
|
|
12
|
-
const delay = retry !== null && Number.isFinite(seconds) ? seconds * 1000 : Date.parse(retry ?? '') - nowMs;
|
|
16
|
+
const delay = retryAfterMs(response.headers.get('retry-after'), nowMs);
|
|
13
17
|
controller.abort();
|
|
14
|
-
return { code: 'rate-limited',
|
|
18
|
+
return { code: 'rate-limited', retryAfterMs: delay };
|
|
15
19
|
}
|
|
16
20
|
if (response.status !== 200) {
|
|
17
21
|
controller.abort();
|
|
@@ -40,11 +44,60 @@ export async function providerGet(source, fetcher, nowMs) {
|
|
|
40
44
|
catch {
|
|
41
45
|
return { code: 'incomplete' };
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
return record(raw) ? { raw } : { code: 'incomplete' };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { code: 'unavailable' };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export async function providerGet(source, fetcher, nowMs) {
|
|
57
|
+
const key = 'access' in source ? source.access : source.key;
|
|
58
|
+
const get = (url, extra) => request(url, key, fetcher, nowMs, extra);
|
|
59
|
+
switch (source.provider) {
|
|
60
|
+
case 'claude': return get('https://api.anthropic.com/api/oauth/usage', { headers: { 'anthropic-beta': 'oauth-2025-04-20' } });
|
|
61
|
+
case 'codex': return get('https://chatgpt.com/backend-api/wham/usage', { headers: { 'ChatGPT-Account-Id': source.accountId } });
|
|
62
|
+
case 'copilot': return get('https://api.github.com/copilot_internal/user');
|
|
63
|
+
case 'minimax': {
|
|
64
|
+
const answer = await get('https://api.minimax.io/v1/token_plan/remains');
|
|
65
|
+
return !answer.code && record(answer.raw) && record(answer.raw.base_resp) && answer.raw.base_resp.status_code !== 0 ? { code: 'no-plan' } : answer;
|
|
66
|
+
}
|
|
67
|
+
case 'kimi': return get('https://api.kimi.com/coding/v1/usages');
|
|
68
|
+
case 'grok': {
|
|
69
|
+
const answer = await get('https://cli-chat-proxy.grok.com/v1/billing?format=credits');
|
|
70
|
+
if (answer.code)
|
|
71
|
+
return answer;
|
|
72
|
+
const config = record(answer.raw) && record(answer.raw.config) ? answer.raw.config : undefined;
|
|
73
|
+
return config?.isUnifiedBillingUser === true || !grokWindows(answer.raw).length ? get('https://cli-chat-proxy.grok.com/v1/billing') : answer;
|
|
74
|
+
}
|
|
75
|
+
case 'gemini': {
|
|
76
|
+
let project = source.project;
|
|
77
|
+
if (!project) {
|
|
78
|
+
const loaded = await get('https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', { body: { metadata: { ideType: 'IDE_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED', pluginType: 'GEMINI' } } });
|
|
79
|
+
if (loaded.code)
|
|
80
|
+
return loaded;
|
|
81
|
+
const value = record(loaded.raw) ? loaded.raw.cloudaicompanionProject : undefined;
|
|
82
|
+
project = typeof value === 'string' ? value : record(value) && typeof value.id === 'string' ? value.id : undefined;
|
|
83
|
+
}
|
|
84
|
+
return get('https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota', { body: project ? { project } : {} });
|
|
85
|
+
}
|
|
86
|
+
case 'opencode': return get('https://opencode.ai/zen/go/v1/usage');
|
|
87
|
+
case 'zai': {
|
|
88
|
+
const answer = await get('https://api.z.ai/api/monitor/usage/quota/limit');
|
|
89
|
+
return !answer.code && record(answer.raw) && answer.raw.success === false ? { code: 'no-plan' } : answer;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A host hook gets the same deadline and failure envelope as built-in sources. */
|
|
94
|
+
export async function customClaude(source, nowMs) {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
let timer;
|
|
97
|
+
try {
|
|
98
|
+
return await Promise.race([source.read({ nowMs, signal: controller.signal }), new Promise((resolve) => {
|
|
99
|
+
timer = setTimeout(() => { controller.abort(); resolve({ code: 'unavailable' }); }, 10_000);
|
|
100
|
+
})]);
|
|
48
101
|
}
|
|
49
102
|
catch {
|
|
50
103
|
return { code: 'unavailable' };
|
|
@@ -107,3 +160,28 @@ export function codexUsage(source) {
|
|
|
107
160
|
child.stdin.write(`${JSON.stringify({ id: 1, method: 'initialize', params: { clientInfo: { name: 'byokit', version: '1' } } })}\n`);
|
|
108
161
|
});
|
|
109
162
|
}
|
|
163
|
+
/** Re-read the tool's credentials so its own renewal is picked up; never renew or write them. */
|
|
164
|
+
export function claudeAuth(source) {
|
|
165
|
+
const stored = readJson(source.credentialsFile, 64 * 1024);
|
|
166
|
+
const credentials = record(stored) && record(stored.claudeAiOauth) ? stored.claudeAiOauth : undefined;
|
|
167
|
+
const token = credentials?.accessToken;
|
|
168
|
+
if (!credentials || typeof token !== 'string' || !token.trim() || token.length > 16384 || token.includes('\0'))
|
|
169
|
+
return undefined;
|
|
170
|
+
const config = source.configFile ? readJson(source.configFile, 4 * 1024 * 1024) : undefined;
|
|
171
|
+
const oauthAccount = record(config) && record(config.oauthAccount) ? config.oauthAccount : undefined;
|
|
172
|
+
const identity = typeof credentials.accountUuid === 'string' ? credentials.accountUuid : typeof oauthAccount?.accountUuid === 'string' ? oauthAccount.accountUuid : `claude-file\0${source.credentialsFile}`;
|
|
173
|
+
if (typeof identity !== 'string' || !identity || identity.length > 16384)
|
|
174
|
+
return undefined;
|
|
175
|
+
return { token, account: identity, ...(typeof credentials.expiresAt === 'number' && Number.isFinite(credentials.expiresAt) ? { expiresAt: credentials.expiresAt } : {}) };
|
|
176
|
+
}
|
|
177
|
+
export async function claudeUsage(source, fetcher, nowMs) {
|
|
178
|
+
const snapshot = source.statuslineFile ? readJsonSnapshot(source.statuslineFile, 64 * 1024) : undefined;
|
|
179
|
+
if (snapshot && nowMs >= snapshot.modified && nowMs - snapshot.modified < 300_000 && claudeWindows(snapshot.value).length)
|
|
180
|
+
return { raw: snapshot.value };
|
|
181
|
+
const auth = claudeAuth(source);
|
|
182
|
+
if (!auth)
|
|
183
|
+
return { code: 'not-connected' };
|
|
184
|
+
if (auth.expiresAt !== undefined && auth.expiresAt <= nowMs)
|
|
185
|
+
return { code: 'expired' };
|
|
186
|
+
return providerGet({ provider: 'claude', access: auth.token }, fetcher, nowMs);
|
|
187
|
+
}
|
package/dist/quota.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Window } from './types.ts';
|
|
2
|
+
export declare function codexTokenWindows(raw: unknown): Window[];
|
|
3
|
+
export declare function copilotWindows(raw: unknown): Window[];
|
|
4
|
+
export declare function grokWindows(raw: unknown): Window[];
|
|
5
|
+
export declare function minimaxWindows(raw: unknown): Window[];
|
|
6
|
+
export declare function geminiWindows(raw: unknown): Window[];
|
|
7
|
+
export declare function kimiWindows(raw: unknown, nowMs: number): Window[];
|