@byokit/usage 0.1.0 → 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/dist/index.js CHANGED
@@ -1,118 +1,277 @@
1
1
  import { accessSync, constants, statSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  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";
4
+ import { claudeAuth, claudeUsage, codexUsage, customClaude, providerGet } from "./providers.js";
5
+ import { fingerprint, readJson, store, memoryUsageStore, safeWindows, safePoll } from "./store.js";
6
+ import { claudeWindows, codexWindows, goWindows, record, zaiWindows } from "./windows.js";
7
+ import { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from "./quota.js";
6
8
  import { UsageError } from "./types.js";
7
9
  export * from "./types.js";
10
+ export { callLedger, normalizeTokens, priceCall } from "./calls.js";
11
+ export { tokenLedger, memoryTokenLedgerStore, TokenLedgerError } from "./ledger.js";
12
+ export { roomOf } from "./room.js";
13
+ export { fingerprint, store as fileUsageStore, memoryUsageStore } from "./store.js";
14
+ export { retryAfterMs, backoffDelayMs, memoryBackoffPolicy } from "./backoff.js";
8
15
  export { claudeWindows, codexWindows, goWindows, zaiWindows } from "./windows.js";
16
+ export { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from "./quota.js";
9
17
  export { WORDS, words, usageWords } from "./words.js";
10
- const validText = (v) => typeof v === 'string' && !v.includes('\0');
18
+ const providers = ['claude', 'codex', 'opencode', 'zai', 'copilot', 'grok', 'minimax', 'gemini', 'kimi'];
19
+ const validText = (v) => typeof v === 'string' && !v.includes('\0') && !/[\r\n]/.test(v) && v.length <= 16384;
11
20
  function validate(source) {
12
- if (!record(source))
21
+ if (!record(source) || !providers.includes(source.provider))
13
22
  throw new UsageError();
14
- if (source.provider === 'codex') {
15
- if (!validText(source.bin) || !isAbsolute(source.bin) || !validText(source.home) || !isAbsolute(source.home))
23
+ if ('credentialsFile' in source) {
24
+ if (source.provider !== 'claude' || ![source.credentialsFile, ...[source.configFile, source.statuslineFile].filter((v) => v !== undefined)].every((v) => validText(v) && isAbsolute(v)))
25
+ throw new UsageError();
26
+ }
27
+ else if ('read' in source) {
28
+ if (source.provider !== 'claude' || typeof source.read !== 'function' || !validText(source.accountUuid) || !source.accountUuid || source.connected !== undefined && typeof source.connected !== 'function')
29
+ throw new UsageError();
30
+ }
31
+ else if ('bin' in source) {
32
+ if (source.provider !== 'codex' || !validText(source.bin) || !isAbsolute(source.bin) || !validText(source.home) || !isAbsolute(source.home))
16
33
  throw new UsageError();
17
34
  if (source.env !== undefined && (!record(source.env) || Object.entries(source.env).some(([k, v]) => !k || k.includes('=') || !validText(k) || !validText(v))))
18
35
  throw new UsageError();
19
36
  }
20
- else if (!['opencode', 'zai'].includes(source.provider) || !validText(source.key) || source.key.length > 16384)
21
- throw new UsageError();
37
+ else {
38
+ if ('key' in source && !['opencode', 'zai'].includes(source.provider) || 'access' in source && ['opencode', 'zai'].includes(source.provider))
39
+ throw new UsageError();
40
+ const credential = 'access' in source ? source.access : 'key' in source ? source.key : undefined;
41
+ if (!validText(credential))
42
+ throw new UsageError();
43
+ if ('access' in source && source.provider === 'codex' && (!validText(source.accountId) || !source.accountId))
44
+ throw new UsageError();
45
+ }
46
+ if ('origin' in source && source.origin !== undefined) {
47
+ try {
48
+ const url = new URL(source.origin);
49
+ if (!['https:', 'http:'].includes(url.protocol) || url.origin !== source.origin)
50
+ throw new Error();
51
+ }
52
+ catch {
53
+ throw new UsageError();
54
+ }
55
+ }
56
+ const fields = source;
57
+ for (const field of ['accountId', 'accountUuid', 'project'])
58
+ if (fields[field] !== undefined && (!validText(fields[field]) || !fields[field]))
59
+ throw new UsageError();
60
+ }
61
+ /** Claims provide a cache identity only; this does not authenticate or trust a token. */
62
+ function subject(access) {
63
+ try {
64
+ const encoded = access.split('.')[1];
65
+ if (!encoded)
66
+ return undefined;
67
+ const claims = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
68
+ return record(claims) && validText(claims.sub) && claims.sub ? claims.sub : undefined;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
22
73
  }
23
74
  /** One reader owns per-account backoff and concurrent-read deduplication. */
24
75
  export function usage(options) {
25
- if (!validText(options.stateDir) || !isAbsolute(options.stateDir))
76
+ if (options.stateDir !== undefined && (!validText(options.stateDir) || !isAbsolute(options.stateDir)))
26
77
  throw new UsageError();
27
- const disk = store(options.stateDir);
78
+ const disk = options.store ?? (options.stateDir ? store(options.stateDir) : memoryUsageStore());
79
+ const transient = memoryUsageStore();
28
80
  const fp = fingerprint(options.salt ?? 'byokit/usage/account');
29
- const backoff = new Map();
81
+ const backoffs = new Map();
30
82
  const inFlight = new Map();
31
83
  const attempts = new Map();
84
+ const failures = new Map();
32
85
  const now = (opts) => opts?.nowMs ?? (options.now ?? Date.now)();
33
86
  function connected(source) {
34
87
  validate(source);
35
- if (source.provider !== 'codex')
36
- return source.key.trim() !== '';
37
- try {
38
- accessSync(source.bin, constants.X_OK);
39
- return statSync(source.bin).isFile();
88
+ if ('credentialsFile' in source)
89
+ return claudeAuth(source) !== undefined;
90
+ if ('read' in source) {
91
+ try {
92
+ return source.connected?.() ?? true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
40
97
  }
41
- catch {
42
- return false;
98
+ if ('bin' in source) {
99
+ try {
100
+ accessSync(source.bin, constants.X_OK);
101
+ return statSync(source.bin).isFile();
102
+ }
103
+ catch {
104
+ return false;
105
+ }
43
106
  }
107
+ return ('access' in source ? source.access : source.key).trim() !== '';
44
108
  }
45
109
  function account(source) {
46
110
  validate(source);
47
- if (source.provider !== 'codex')
48
- return source.key.trim() ? fp(source.provider, source.key) : undefined;
49
- const raw = readJson(join(source.home, 'auth.json'), 64 * 1024);
50
- const tokens = record(raw) && record(raw.tokens) ? raw.tokens : undefined;
51
- const value = typeof tokens?.account_id === 'string' ? tokens.account_id : `codex-home\0${source.home}`;
52
- return value !== '' && value.length <= 16384 ? fp('codex', value) : undefined;
111
+ let id;
112
+ if ('credentialsFile' in source)
113
+ id = claudeAuth(source)?.account;
114
+ else if ('read' in source)
115
+ id = source.accountUuid;
116
+ else if ('bin' in source) {
117
+ const raw = readJson(join(source.home, 'auth.json'), 64 * 1024);
118
+ const tokens = record(raw) && record(raw.tokens) ? raw.tokens : undefined;
119
+ id = typeof tokens?.account_id === 'string' ? tokens.account_id : `codex-home\0${source.home}`;
120
+ }
121
+ else
122
+ id = source.accountId ?? (source.provider === 'claude' ? source.accountUuid : undefined) ?? ('access' in source ? subject(source.access) : undefined);
123
+ return id && id.length <= 16384 ? fp(source.provider, id) : undefined;
124
+ }
125
+ function identity(source) {
126
+ const stable = account(source);
127
+ if (stable)
128
+ return { key: stable, stable: true };
129
+ // Opaque credentials without an identity can be read, but never reach an app/disk store.
130
+ const credential = 'access' in source ? source.access : 'key' in source ? source.key : '';
131
+ return { key: createHash('sha256').update(`${source.provider}\0${credential}`).digest('hex'), stable: false };
132
+ }
133
+ function windows(source, raw, clock) {
134
+ switch (source.provider) {
135
+ case 'claude': return claudeWindows(raw);
136
+ case 'codex': return 'bin' in source ? codexWindows(raw) : codexTokenWindows(raw, clock);
137
+ case 'opencode': return goWindows(record(raw) ? raw.usage : undefined);
138
+ case 'zai': return zaiWindows(record(raw) && record(raw.data) ? raw.data.limits : undefined);
139
+ case 'copilot': return copilotWindows(raw);
140
+ case 'grok': return grokWindows(raw);
141
+ case 'minimax': return minimaxWindows(raw);
142
+ case 'gemini': return geminiWindows(raw);
143
+ case 'kimi': return kimiWindows(raw, clock);
144
+ }
53
145
  }
54
- const windows = (source, raw) => source.provider === 'codex' ? codexWindows(raw) : source.provider === 'opencode' ? goWindows(raw) : zaiWindows(raw);
55
146
  function lastKnown(source, opts) {
56
147
  if (!connected(source))
57
148
  return undefined;
58
- const key = account(source);
59
- if (!key)
60
- return undefined;
61
- const stored = disk.get(source.provider, key);
149
+ const id = identity(source);
62
150
  const clock = now(opts);
63
- if (!stored || clock < stored.at || clock - stored.at > 86_400_000)
151
+ try {
152
+ const stored = (id.stable ? disk : transient).get(source.provider, id.key);
153
+ if (!stored)
154
+ return undefined;
155
+ if (!stored.limited && !stored.windows.some((w) => w.limited) && stored.at !== undefined && (clock < stored.at || clock - stored.at > 86_400_000))
156
+ return undefined;
157
+ const rows = safeWindows(source.provider, stored.windows);
158
+ const poll = attempts.get(`${source.provider}\0${id.key}`) ?? safePoll(stored.poll);
159
+ return rows.length || stored.limited ? { provider: source.provider, windows: rows, ...(stored.at !== undefined ? { at: stored.at } : {}),
160
+ ...(stored.limited ? { limited: true } : {}), ...(poll ? { poll, ...(poll.outcome !== 'ok' ? { code: poll.outcome } : {}) } : {}) } : undefined;
161
+ }
162
+ catch {
64
163
  return undefined;
65
- const rows = windows(source, stored.raw);
66
- return rows.length ? { provider: source.provider, windows: rows, at: stored.at } : undefined;
164
+ }
67
165
  }
68
166
  async function read(source, opts) {
69
167
  validate(source);
70
168
  const clock = now(opts);
71
- const empty = (code) => ({ provider: source.provider, windows: [], at: clock, code });
169
+ const empty = (code) => ({ provider: source.provider, windows: [], code, poll: { at: clock, outcome: code ?? 'unavailable' } });
72
170
  if (!connected(source))
73
171
  return empty('not-connected');
74
- const key = account(source);
75
- if (!key)
76
- return empty('not-connected');
77
- const identity = `${source.provider}\0${key}`;
172
+ const id = identity(source);
173
+ const key = `${source.provider}\0${id.key}`;
78
174
  const previous = lastKnown(source, { nowMs: clock });
79
- if (previous && clock - previous.at < 60_000)
175
+ if (previous && !previous.code && previous.at !== undefined && clock >= previous.at && clock - previous.at < 60_000)
80
176
  return previous;
81
- if ((backoff.get(identity) ?? 0) > clock)
82
- return previous ? { ...previous, code: 'rate-limited' } : empty('rate-limited');
83
- const pending = inFlight.get(identity);
177
+ let until = backoffs.get(key) ?? 0;
178
+ try {
179
+ const saved = id.stable ? options.backoff?.get(source.provider, id.key) : undefined;
180
+ if (typeof saved === 'number' && Number.isFinite(saved))
181
+ until = Math.max(until, saved);
182
+ else if (saved && typeof saved !== 'number' && Number.isFinite(saved.untilMs) && Number.isFinite(saved.at) && ['rate-limited', 'refresh-failed', 'unavailable', 'incomplete'].includes(saved.outcome)) {
183
+ until = Math.max(until, saved.untilMs);
184
+ if (!attempts.has(key))
185
+ attempts.set(key, { at: saved.at, outcome: saved.outcome, retryAt: saved.untilMs });
186
+ if (!failures.has(`${key}\0${saved.outcome}`) && Number.isSafeInteger(saved.failures) && saved.failures > 0)
187
+ failures.set(`${key}\0${saved.outcome}`, saved.failures);
188
+ }
189
+ }
190
+ catch { /* keep internal backoff */ }
191
+ if (until > clock) {
192
+ const poll = attempts.get(key) ?? previous?.poll ?? { at: clock, outcome: 'unavailable', retryAt: until };
193
+ return { ...(previous ?? empty(poll.outcome === 'ok' ? 'unavailable' : poll.outcome)), code: poll.outcome === 'ok' ? undefined : poll.outcome, poll };
194
+ }
195
+ const pending = inFlight.get(key);
84
196
  if (pending)
85
197
  return pending;
86
- const attempt = attempts.get(identity);
198
+ const attempt = attempts.get(key) ?? previous?.poll;
87
199
  if (attempt && clock >= attempt.at && clock - attempt.at < 60_000)
88
- return previous ? { ...previous, code: attempt.answer.code } : empty(attempt.answer.code ?? 'unavailable');
200
+ return previous ? { ...previous, code: attempt.outcome === 'ok' ? undefined : attempt.outcome, poll: attempt } : empty(attempt.outcome === 'ok' ? 'unavailable' : attempt.outcome);
89
201
  const task = (async () => {
90
202
  let answer;
203
+ const pacing = { hook: options.pace, provider: source.provider, account: id.key, signal: opts?.signal };
91
204
  try {
92
- answer = source.provider === 'codex' ? await codexUsage(source) : await providerGet(source, options.fetch ?? globalThis.fetch, clock);
205
+ answer = 'read' in source ? await customClaude(source, clock, pacing) : 'credentialsFile' in source ? await claudeUsage(source, options.fetch ?? globalThis.fetch, clock, pacing)
206
+ : 'bin' in source ? await codexUsage(source) : await providerGet(source, options.fetch ?? globalThis.fetch, clock, pacing);
93
207
  }
94
208
  catch {
95
209
  answer = { code: 'unavailable' };
96
210
  }
97
- if (answer.until !== undefined)
98
- backoff.set(identity, answer.until);
99
- const rows = windows(source, answer.raw);
100
- if (!answer.code && !rows.length)
211
+ const rows = safeWindows(source.provider, windows(source, answer.raw, clock));
212
+ const limited = !answer.code && (answer.limited === true || source.provider === 'codex' && codexHardLimit(answer.raw));
213
+ if (!answer.code && !rows.length && !limited)
101
214
  answer = { code: 'incomplete' };
102
- // Do not keep a source key or a raw payload in a retry map.
103
- attempts.set(identity, { at: clock, answer: { code: answer.code } });
104
- if (rows.length && !answer.code) {
105
- disk.put(source.provider, key, { at: clock, raw: answer.raw });
106
- return { provider: source.provider, windows: rows, at: clock };
215
+ const poll = { at: clock, outcome: answer.code ?? 'ok' };
216
+ if (answer.code) {
217
+ const failureKey = `${key}\0${answer.code}`;
218
+ const count = (failures.get(failureKey) ?? 0) + 1;
219
+ failures.set(failureKey, count);
220
+ if (['rate-limited', 'refresh-failed', 'unavailable', 'incomplete'].includes(answer.code)) {
221
+ const retry = Number.isFinite(answer.retryAfterMs) && answer.retryAfterMs >= 0 ? answer.retryAfterMs : undefined;
222
+ let delay = answer.code === 'rate-limited' ? 300_000 : Math.min(3_600_000, 60_000 * 2 ** Math.min(count - 1, 6));
223
+ try {
224
+ const selected = options.backoff?.delayMs?.(retry, { outcome: answer.code, failures: count });
225
+ if (selected !== undefined && Number.isFinite(selected) && selected >= 0)
226
+ delay = selected;
227
+ }
228
+ catch { /* internal policy stands */ }
229
+ delay = Math.max(60_000, delay, retry ?? 0);
230
+ poll.retryAt = clock + delay;
231
+ backoffs.set(key, poll.retryAt);
232
+ try {
233
+ if (id.stable)
234
+ options.backoff?.set(source.provider, id.key, poll.retryAt, { untilMs: poll.retryAt, at: clock, outcome: answer.code, failures: count });
235
+ }
236
+ catch { /* internal backoff stands */ }
237
+ }
238
+ }
239
+ else {
240
+ for (const outcome of ['rate-limited', 'refresh-failed', 'unavailable', 'incomplete'])
241
+ failures.delete(`${key}\0${outcome}`);
242
+ backoffs.delete(key);
243
+ try {
244
+ if (id.stable)
245
+ options.backoff?.set(source.provider, id.key, 0);
246
+ }
247
+ catch { /* expired host policy remains bounded */ }
248
+ }
249
+ attempts.set(key, poll);
250
+ if ((rows.length || limited) && !answer.code) {
251
+ // Explicit host/snapshot time is authoritative, including unknown or future time.
252
+ const observed = 'at' in answer ? answer.at : clock;
253
+ const at = typeof observed === 'number' && Number.isFinite(observed) ? observed : undefined;
254
+ const reading = { provider: source.provider, windows: rows, ...(at !== undefined ? { at } : {}), ...(limited ? { limited: true } : {}), poll };
255
+ try {
256
+ (id.stable ? disk : transient).put(source.provider, id.key, { at: reading.at, windows: rows, ...(limited ? { limited: true } : {}), poll });
257
+ }
258
+ catch { /* reads survive a store failure */ }
259
+ return reading;
260
+ }
261
+ if (previous) {
262
+ try {
263
+ (id.stable ? disk : transient).put(source.provider, id.key, { at: previous.at, windows: previous.windows, ...(previous.limited ? { limited: true } : {}), poll });
264
+ }
265
+ catch { /* last-good remains in memory */ }
107
266
  }
108
- return previous ? { ...previous, code: answer.code } : empty(answer.code ?? 'unavailable');
267
+ return { ...(previous ?? empty(answer.code ?? 'unavailable')), code: answer.code, poll };
109
268
  })();
110
- inFlight.set(identity, task);
269
+ inFlight.set(key, task);
111
270
  try {
112
271
  return await task;
113
272
  }
114
273
  finally {
115
- inFlight.delete(identity);
274
+ inFlight.delete(key);
116
275
  }
117
276
  }
118
277
  return { read, lastKnown, connected, account };
@@ -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
+ }
@@ -1,12 +1,41 @@
1
- import type { Code, Source } from './types.ts';
2
- export interface Answer {
3
- raw?: unknown;
4
- code?: Code;
5
- until?: number;
6
- }
7
- export declare function providerGet(source: Extract<Source, {
1
+ import type { Source, SourceAnswer, PacingHook } from './types.ts';
2
+ export type Answer = SourceAnswer;
3
+ type TokenSource = Extract<Source, {
4
+ access: string;
5
+ } | {
8
6
  key: string;
9
- }>, fetcher: typeof fetch, nowMs: number): Promise<Answer>;
7
+ }>;
8
+ export declare function providerGet(source: TokenSource, fetcher: typeof fetch, nowMs: number, pacing?: {
9
+ hook?: PacingHook;
10
+ provider: Source['provider'];
11
+ account: string;
12
+ signal?: AbortSignal;
13
+ }): Promise<Answer>;
14
+ /** A host hook gets the same deadline and failure envelope as built-in sources. */
15
+ export declare function customClaude(source: Extract<Source, {
16
+ read: unknown;
17
+ }>, nowMs: number, pacing?: {
18
+ hook?: PacingHook;
19
+ account: string;
20
+ signal?: AbortSignal;
21
+ }): Promise<Answer>;
10
22
  export declare function codexUsage(source: Extract<Source, {
11
- provider: 'codex';
23
+ bin: string;
12
24
  }>): Promise<Answer>;
25
+ /** Re-read the tool's credentials so its own renewal is picked up; never renew or write them. */
26
+ export declare function claudeAuth(source: Extract<Source, {
27
+ credentialsFile: string;
28
+ }>): {
29
+ token: string;
30
+ expiresAt?: number;
31
+ account: string;
32
+ } | undefined;
33
+ export declare function claudeUsage(source: Extract<Source, {
34
+ credentialsFile: string;
35
+ }>, fetcher: typeof fetch, nowMs: number, pacing?: {
36
+ hook?: PacingHook;
37
+ provider: Source['provider'];
38
+ account: string;
39
+ signal?: AbortSignal;
40
+ }): Promise<Answer>;
41
+ export {};