@byokit/usage 0.5.0 → 0.6.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 +4 -0
- package/README.md +25 -0
- package/dist/ephemeral.d.ts +3 -0
- package/dist/ephemeral.js +64 -0
- package/dist/index.js +11 -2
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +1 -1
- package/dist/types.d.ts +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.0 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
- FIX: Claude subscription quota snapshots can now be read through an identity-free ephemeral host callback without credentials or a fabricated account UUID; readings never enter a cache or shared store, and retry state stays local to the source.
|
|
8
|
+
|
|
5
9
|
## 0.5.0 (2026-10-01)
|
|
6
10
|
|
|
7
11
|
- Add a React Native entry for local call/token ledgers and pure quota helpers; subscription and API key (billed per use) attribution is preserved, with estimates only from app prices.
|
package/README.md
CHANGED
|
@@ -39,6 +39,26 @@ Sources:
|
|
|
39
39
|
host reader's optional HTTP origin for pacing. It has a ten-second deadline, with the signal
|
|
40
40
|
aborted at expiry. The optional synchronous `connected()` hook controls whether
|
|
41
41
|
last-good readings remain visible; exceptions count as disconnected.
|
|
42
|
+
- `{ provider: 'claude', ephemeral: true, read, connected? }` supports a host-owned
|
|
43
|
+
opaque snapshot stream without an account UUID, email, folder or token. The same
|
|
44
|
+
`ClaudeReader` answer and ten-second cancellable deadline apply. For a statusline
|
|
45
|
+
body, return `{ raw: snapshot, at: snapshot.fetched_at }` when the host knows that
|
|
46
|
+
`fetched_at` is epoch milliseconds; otherwise omit `at` for unknown age. The kit
|
|
47
|
+
normalizes `rate_limits` and quota rows; it never opens a snapshot or credential
|
|
48
|
+
file, discovers credentials, copies tokens or makes a fallback request.
|
|
49
|
+
`connected()` should reflect the host's sign-in status; false or a thrown error
|
|
50
|
+
returns `not-connected`. An absent/invalid snapshot returns `incomplete`, while
|
|
51
|
+
timeout/cancellation returns `unavailable`; the host can report other safe codes.
|
|
52
|
+
`account()` and `lastKnown()` always return undefined. Successful readings are
|
|
53
|
+
never retained, so each subsequent read invokes the callback again. Only retry
|
|
54
|
+
metadata and concurrent operations are weakly held per source object in this
|
|
55
|
+
reader: reuse an immutable source for one stream, and replace it when the host
|
|
56
|
+
changes sign-in or stream. Failures never return earlier quota figures.
|
|
57
|
+
Rate limits honor Retry-After with a five-minute default; transient failures use
|
|
58
|
+
exponential backoff from one minute, capped at one hour. `backoff.delayMs` may
|
|
59
|
+
customize these delays with a one-minute minimum. Store and backoff get/set
|
|
60
|
+
hooks and account-based pacing are never called; the host owns any pacing in
|
|
61
|
+
its callback. No state is shared between source objects or reader instances.
|
|
42
62
|
|
|
43
63
|
`read(source, { nowMs?, signal? })` returns `{ provider, windows, at?, limited?, poll?, code? }`.
|
|
44
64
|
`at` is source observation time; it is absent when unavailable. `poll` contains
|
|
@@ -359,6 +379,11 @@ that the accounts chooser accepts directly. Preserve the original measurement ti
|
|
|
359
379
|
|
|
360
380
|
Managed-folder Claude usage uses the shared poll-health and normalized quota pipeline, including scoped hard blocks, unknown usage, last-good observation times, account retry policies and cancellable host origin pacing.
|
|
361
381
|
|
|
382
|
+
The token and call ledgers accept host-supplied entries/results; they are not
|
|
383
|
+
harness-log or ccusage readers. A bounded incremental log source (including file
|
|
384
|
+
offsets, rotation and bounded work per update) remains outstanding. This snapshot
|
|
385
|
+
source does not implement it or scan real logs.
|
|
386
|
+
|
|
362
387
|
## React Native
|
|
363
388
|
|
|
364
389
|
The `react-native` condition of `@byokit/usage` selects a portable entry. The explicit
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EphemeralClaudeSource, Reading, ReadOptions, UsageOptions } from './types.ts';
|
|
2
|
+
/** Only retry/concurrency metadata survives a call; source objects are weakly held. */
|
|
3
|
+
export declare function ephemeralClaude(options: UsageOptions): (source: EphemeralClaudeSource, clock: number, opts?: ReadOptions) => Promise<Reading>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { customClaude } from "./providers.js";
|
|
2
|
+
import { safeWindows } from "./safe-windows.js";
|
|
3
|
+
import { claudeWindows } from "./windows.js";
|
|
4
|
+
/** Only retry/concurrency metadata survives a call; source objects are weakly held. */
|
|
5
|
+
export function ephemeralClaude(options) {
|
|
6
|
+
const states = new WeakMap();
|
|
7
|
+
return async (source, clock, opts) => {
|
|
8
|
+
const empty = (poll) => ({ provider: 'claude', windows: [], code: poll.outcome === 'ok' ? 'unavailable' : poll.outcome, poll });
|
|
9
|
+
let connected = true;
|
|
10
|
+
try {
|
|
11
|
+
connected = source.connected?.() ?? true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
connected = false;
|
|
15
|
+
}
|
|
16
|
+
if (!connected) {
|
|
17
|
+
states.delete(source);
|
|
18
|
+
return empty({ at: clock, outcome: 'not-connected' });
|
|
19
|
+
}
|
|
20
|
+
let state = states.get(source);
|
|
21
|
+
if (!state) {
|
|
22
|
+
state = { failures: 0 };
|
|
23
|
+
states.set(source, state);
|
|
24
|
+
}
|
|
25
|
+
if (state.pending)
|
|
26
|
+
return state.pending;
|
|
27
|
+
if (state.poll?.retryAt !== undefined && clock < state.poll.retryAt)
|
|
28
|
+
return empty(state.poll);
|
|
29
|
+
const current = state;
|
|
30
|
+
const task = (async () => {
|
|
31
|
+
const answer = await customClaude(source, clock, { signal: opts?.signal });
|
|
32
|
+
const rows = safeWindows('claude', claudeWindows(answer.raw));
|
|
33
|
+
const limited = !answer.code && answer.limited === true;
|
|
34
|
+
const code = answer.code ?? (!rows.length && !limited ? 'incomplete' : undefined);
|
|
35
|
+
const poll = { at: clock, outcome: code ?? 'ok' };
|
|
36
|
+
if (code && ['rate-limited', 'refresh-failed', 'unavailable', 'incomplete'].includes(code)) {
|
|
37
|
+
current.failures = current.poll?.outcome === code ? current.failures + 1 : 1;
|
|
38
|
+
const retry = typeof answer.retryAfterMs === 'number' && Number.isFinite(answer.retryAfterMs) && answer.retryAfterMs >= 0 ? answer.retryAfterMs : undefined;
|
|
39
|
+
let delay = code === 'rate-limited' ? 300_000 : Math.min(3_600_000, 60_000 * 2 ** Math.min(current.failures - 1, 6));
|
|
40
|
+
try {
|
|
41
|
+
const selected = options.backoff?.delayMs?.(retry, { outcome: code, failures: current.failures });
|
|
42
|
+
if (selected !== undefined && Number.isFinite(selected) && selected >= 0)
|
|
43
|
+
delay = selected;
|
|
44
|
+
}
|
|
45
|
+
catch { /* internal retry policy stands */ }
|
|
46
|
+
poll.retryAt = clock + Math.max(60_000, delay, retry ?? 0);
|
|
47
|
+
}
|
|
48
|
+
else
|
|
49
|
+
current.failures = 0;
|
|
50
|
+
current.poll = poll;
|
|
51
|
+
if (code)
|
|
52
|
+
return empty(poll);
|
|
53
|
+
const at = typeof answer.at === 'number' && Number.isFinite(answer.at) ? answer.at : undefined;
|
|
54
|
+
return { provider: 'claude', windows: rows, ...(at !== undefined ? { at } : {}), ...(limited ? { limited: true } : {}), poll };
|
|
55
|
+
})();
|
|
56
|
+
current.pending = task;
|
|
57
|
+
try {
|
|
58
|
+
return await task;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
current.pending = undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimax
|
|
|
8
8
|
import { UsageError } from "./types.js";
|
|
9
9
|
import { codexIdentity } from "./identity.js";
|
|
10
10
|
import { claudeUsage as managedClaudeUsage, claudeCredential, managedClaudeFolder } from "./claude.js";
|
|
11
|
+
import { ephemeralClaude } from "./ephemeral.js";
|
|
11
12
|
export * from "./types.js";
|
|
12
13
|
/** Identity runs only the named binary, never opens a credential file. */
|
|
13
14
|
export async function identity(source) {
|
|
@@ -29,6 +30,8 @@ const validText = (v) => typeof v === 'string' && !v.includes('\0') && !/[\r\n]/
|
|
|
29
30
|
function validate(source, stateDir) {
|
|
30
31
|
if (!record(source) || !providers.includes(source.provider))
|
|
31
32
|
throw new UsageError();
|
|
33
|
+
if ('ephemeral' in source && (source.ephemeral !== true || source.provider !== 'claude' || typeof source.read !== 'function' || ['accountUuid', 'accountId', 'origin', 'folder', 'credentialsFile', 'access', 'key', 'bin', 'home', 'configFile', 'statuslineFile'].some((field) => field in source)))
|
|
34
|
+
throw new UsageError();
|
|
32
35
|
if ('folder' in source) {
|
|
33
36
|
if (source.provider !== 'claude' || !validText(source.folder) || !isAbsolute(source.folder) || !stateDir || !managedClaudeFolder(source.folder, stateDir))
|
|
34
37
|
throw new UsageError();
|
|
@@ -40,7 +43,7 @@ function validate(source, stateDir) {
|
|
|
40
43
|
throw new UsageError();
|
|
41
44
|
}
|
|
42
45
|
else if ('read' in source) {
|
|
43
|
-
if (source.provider !== 'claude' || typeof source.read !== 'function' || !validText(source.accountUuid) || !source.accountUuid || source.connected !== undefined && typeof source.connected !== 'function')
|
|
46
|
+
if (source.provider !== 'claude' || typeof source.read !== 'function' || !('ephemeral' in source) && (!validText(source.accountUuid) || !source.accountUuid) || source.connected !== undefined && typeof source.connected !== 'function')
|
|
44
47
|
throw new UsageError();
|
|
45
48
|
}
|
|
46
49
|
else if ('bin' in source) {
|
|
@@ -98,6 +101,7 @@ export function usage(options) {
|
|
|
98
101
|
const attempts = new Map();
|
|
99
102
|
const failures = new Map();
|
|
100
103
|
const now = (opts) => opts?.nowMs ?? (options.now ?? Date.now)();
|
|
104
|
+
const readEphemeral = ephemeralClaude(options);
|
|
101
105
|
function connected(source) {
|
|
102
106
|
validate(source, options.stateDir);
|
|
103
107
|
if ('folder' in source)
|
|
@@ -133,7 +137,7 @@ export function usage(options) {
|
|
|
133
137
|
else if ('credentialsFile' in source)
|
|
134
138
|
id = claudeAuth(source)?.account;
|
|
135
139
|
else if ('read' in source)
|
|
136
|
-
id = source.accountUuid;
|
|
140
|
+
id = 'ephemeral' in source ? undefined : source.accountUuid;
|
|
137
141
|
else if ('bin' in source) {
|
|
138
142
|
const raw = readJson(join(source.home, 'auth.json'), 64 * 1024);
|
|
139
143
|
const tokens = record(raw) && record(raw.tokens) ? raw.tokens : undefined;
|
|
@@ -165,6 +169,9 @@ export function usage(options) {
|
|
|
165
169
|
}
|
|
166
170
|
}
|
|
167
171
|
function lastKnown(source, opts) {
|
|
172
|
+
validate(source, options.stateDir);
|
|
173
|
+
if ('ephemeral' in source)
|
|
174
|
+
return undefined;
|
|
168
175
|
if (!connected(source))
|
|
169
176
|
return undefined;
|
|
170
177
|
const id = identity(source);
|
|
@@ -187,6 +194,8 @@ export function usage(options) {
|
|
|
187
194
|
async function read(source, opts) {
|
|
188
195
|
validate(source, options.stateDir);
|
|
189
196
|
const clock = now(opts);
|
|
197
|
+
if ('ephemeral' in source)
|
|
198
|
+
return readEphemeral(source, clock, opts);
|
|
190
199
|
const empty = (code) => ({ provider: source.provider, windows: [], code, poll: { at: clock, outcome: code ?? 'unavailable' } });
|
|
191
200
|
if (!connected(source))
|
|
192
201
|
return empty('not-connected');
|
package/dist/providers.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare function customClaude(source: Extract<Source, {
|
|
|
26
26
|
read: unknown;
|
|
27
27
|
}>, nowMs: number, pacing?: {
|
|
28
28
|
hook?: PacingHook;
|
|
29
|
-
account
|
|
29
|
+
account?: string;
|
|
30
30
|
signal?: AbortSignal;
|
|
31
31
|
}): Promise<Answer>;
|
|
32
32
|
export declare function codexUsage(source: Extract<Source, {
|
package/dist/providers.js
CHANGED
|
@@ -114,7 +114,7 @@ export async function customClaude(source, nowMs, pacing) {
|
|
|
114
114
|
const timer = setTimeout(abort, 10_000);
|
|
115
115
|
try {
|
|
116
116
|
const operation = async () => {
|
|
117
|
-
if (source.origin && pacing?.hook)
|
|
117
|
+
if ('origin' in source && source.origin && pacing?.hook && pacing.account !== undefined)
|
|
118
118
|
await pacing.hook({ provider: source.provider, account: pacing.account, origin: source.origin, signal: controller.signal });
|
|
119
119
|
if (controller.signal.aborted)
|
|
120
120
|
return { code: 'unavailable' };
|
package/dist/types.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type Source = {
|
|
|
29
29
|
read: ClaudeReader;
|
|
30
30
|
origin?: string;
|
|
31
31
|
connected?: () => boolean;
|
|
32
|
-
}) | {
|
|
32
|
+
}) | EphemeralClaudeSource | {
|
|
33
33
|
provider: 'claude';
|
|
34
34
|
credentialsFile: string;
|
|
35
35
|
configFile?: string;
|
|
@@ -106,6 +106,13 @@ export type ClaudeReader = (options: {
|
|
|
106
106
|
nowMs: number;
|
|
107
107
|
signal: AbortSignal;
|
|
108
108
|
}) => Promise<SourceAnswer>;
|
|
109
|
+
/** Reuse one source object per host-owned snapshot stream; no identity or last-good cache. */
|
|
110
|
+
export type EphemeralClaudeSource = {
|
|
111
|
+
provider: 'claude';
|
|
112
|
+
ephemeral: true;
|
|
113
|
+
read: ClaudeReader;
|
|
114
|
+
connected?: () => boolean;
|
|
115
|
+
};
|
|
109
116
|
export type StoredReading = {
|
|
110
117
|
at?: number;
|
|
111
118
|
windows: Window[];
|