@byokit/usage 0.3.0 → 0.5.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 +14 -0
- package/README.md +139 -2
- package/dist/calls.d.ts +18 -3
- package/dist/calls.js +49 -16
- package/dist/claude.d.ts +12 -0
- package/dist/claude.js +47 -0
- package/dist/identity.d.ts +11 -0
- package/dist/identity.js +16 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +28 -7
- package/dist/providers.d.ts +14 -0
- package/dist/providers.js +8 -4
- package/dist/rn.d.ts +8 -0
- package/dist/rn.js +7 -0
- package/dist/safe-windows.d.ts +3 -0
- package/dist/safe-windows.js +14 -0
- package/dist/store.d.ts +2 -3
- package/dist/store.js +3 -14
- package/dist/types.d.ts +7 -0
- package/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.0 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.4.0 (2026-10-01)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
- SECURITY: Claude subscription usage may read credentials only from app-managed folders under the passed stateDir, without refresh or credential writes; default logins and folder escapes are refused, tokens stay in one request and never enter output, errors, logs or stored readings.
|
|
14
|
+
- Share the bounded Codex app-server client between identity and subscription usage reads.
|
|
15
|
+
- Apply shared poll health, scoped quota and hard-limit semantics to managed Claude usage, including cancellable host pacing.
|
|
16
|
+
- Add host lane/route attribution and member-scoped per-run token queries to `callLedger`, sharing existing limits and app-owned cap policy.
|
|
17
|
+
- Accept OpenClaw run usage with separate cache buckets alongside accounts and decide results, without provider calls or logging. Subscription attribution defaults on; API key (billed per use) attribution stays explicit and labelled.
|
|
18
|
+
|
|
5
19
|
## 0.3.0 (2026-10-01)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @byokit/usage
|
|
2
2
|
|
|
3
3
|
Read subscription quota windows per provider and per account on Node 22.18 or later.
|
|
4
|
+
React Native also supports local call/token accounting and pure quota parsing.
|
|
4
5
|
The app owns sign-in, token renewal, account labels and selection. The kit reads room
|
|
5
6
|
left, estimates no cost and never rotates an account.
|
|
6
7
|
|
|
@@ -47,6 +48,18 @@ original observation time. Poll 429 (`rate-limited`), host renewal failure
|
|
|
47
48
|
(`refresh-failed`) and credential refusals are distinct; usage never changes
|
|
48
49
|
account health or renews credentials.
|
|
49
50
|
|
|
51
|
+
The managed-folder source is `{ provider: 'claude', folder, headers: { 'anthropic-beta', 'User-Agent' } }`. Paths must be absolute; removed or empty keys mean disconnected. Claude headers are app-passed. The Claude folder must resolve inside the reader's `stateDir` as `<stateDir>/claude/<hex>`, matching managed CLI account folders. Default `.claude` roots, paths outside the root, and symlinked folders or credential files are refused. Use the managed plans root as the usage reader's stateDir; its quota store coexists with the account roster. The default login's usage adapter remains with the host.
|
|
52
|
+
|
|
53
|
+
Approved exception: the `./cli` entry reads and runs only app-managed per-account folders under `stateDir` and the absolute CLI binaries the app passes; it never touches the person's default login; tokens never leave the device and are never logged. Under the same managed-folder boundary, usage may read `.credentials.json` only for a single Claude subscription usage request. It does not refresh, write, rename or copy credentials; expired or malformed credentials return `expired` or `not-connected`, requiring sign-in again. The token is held in memory only for that request and never enters readings, stored quotas, errors or logs. Folder and credential metadata supply the cache fingerprint without loading a token, and credential changes invalidate the cached account reading.
|
|
54
|
+
|
|
55
|
+
`read(source, { nowMs? })` returns `{ provider, windows, at, code? }`; `at` and all
|
|
56
|
+
`resetsAt` fields are **epoch milliseconds** in 0.2.0. This changes 0.1.0's seconds
|
|
57
|
+
reset convention. Windows include kind, used percent, optional duration in minutes,
|
|
58
|
+
reset time, limit label and limited flag. Parsers are exported for host integrations:
|
|
59
|
+
`claudeWindows`, `codexWindows` (app-server), `codexTokenWindows`, `goWindows`,
|
|
60
|
+
`zaiWindows`, `copilotWindows`, `grokWindows`, `minimaxWindows`, `geminiWindows`,
|
|
61
|
+
`kimiWindows(raw, nowMs)`.
|
|
62
|
+
|
|
50
63
|
Windows include kind, optional reported `usedPercent`, duration, reset, limit,
|
|
51
64
|
`limited` and `scope: { model?, surface? }`. Missing usage is unknown, never zero.
|
|
52
65
|
Claude `limits[]` session/weekly-all rows override corresponding legacy aggregates,
|
|
@@ -197,6 +210,8 @@ and HOME explicitly when needed. No tokens in logs, errors, readings or public h
|
|
|
197
210
|
no credential write-back, telemetry, automatic refresh or reset-credit spend.
|
|
198
211
|
|
|
199
212
|
Built-in requests send `User-Agent: byokit/usage/0.3.0`, never another app's identity.
|
|
213
|
+
|
|
214
|
+
Token and explicit-file sources send `User-Agent: byokit/usage/0.2.0`, never another app's identity.
|
|
200
215
|
A refusal returns a code; with no last-good quota, room is unknown. Fixed endpoints
|
|
201
216
|
are Anthropic `api/oauth/usage`, ChatGPT `backend-api/wham/usage`, GitHub
|
|
202
217
|
`copilot_internal/user`, Grok `v1/billing` (weekly credits then monthly when needed),
|
|
@@ -228,8 +243,11 @@ or store exception text. Entries are counts only, never sign-in tokens.
|
|
|
228
243
|
|
|
229
244
|
`callLedger({ store?, prices? })` records runtime model calls through the same
|
|
230
245
|
`TokenLedgerStore` seam. `record(member, { provider, account, model, runId, time,
|
|
231
|
-
billing
|
|
232
|
-
|
|
246
|
+
billing?, usage?, usageFormat?, lane?, route?, payer?, durationMs?, state?, limits? })`
|
|
247
|
+
returns and stores one `CallRecord`. `billing` defaults to `subscription`; passing
|
|
248
|
+
`api` explicitly attributes an API key (billed per use) call. Every record carries
|
|
249
|
+
`billingLabel: "Person's own plan" | "Person's API bill"`, even without a price
|
|
250
|
+
estimate. `payer` defaults to the member.
|
|
233
251
|
`state` is `completed` (default), `cancelled` or `failed`. The host records each
|
|
234
252
|
actual model call, including retries, and supplies the provider's final usage when
|
|
235
253
|
available. No missing counts are inferred from words or decision sub-answers.
|
|
@@ -264,8 +282,127 @@ calls with unknown total counts, member/day/week results expose `unknownCalls`,
|
|
|
264
282
|
policy remain the host's. All times, durations and quota reset timestamps are
|
|
265
283
|
milliseconds. There is no transport, credential discovery or automatic rotation.
|
|
266
284
|
|
|
285
|
+
Record host lanes and routes with optional app-supplied `lane` and `route` fields.
|
|
286
|
+
`runs(member, from, to)` returns a `RunQuery` per run in first-call order;
|
|
287
|
+
`queryRun(member, runId, from, to)` returns one run. Each result contains `runId`,
|
|
288
|
+
time-sorted `calls` (including lane, route, model and limits), aggregate `tokens`,
|
|
289
|
+
`costs` and `unpricedCalls`, using the same `[from, to)` bounds as `query`.
|
|
290
|
+
An absent run returns no calls and zero tokens. A run's totals cover only calls
|
|
291
|
+
inside the requested range; pass the run's full time range for its complete total.
|
|
292
|
+
Retries and multiple routes/models are added under the app's run id. Members remain
|
|
293
|
+
separate even when run ids match. Missing counts stay unknown in run totals, and
|
|
294
|
+
the shared member ledger continues to withhold remaining allowance when needed.
|
|
295
|
+
|
|
296
|
+
Pass a result with a `usage` field directly, or pass just its usage. For accounts'
|
|
297
|
+
Messages result and decide's reported answer usage, the default provider format
|
|
298
|
+
handles native `input_tokens`/`output_tokens` counts. For OpenClaw `RunEnd`, pass
|
|
299
|
+
`usageFormat: 'openclaw'`: its `input` excludes `cacheRead`/`cacheWrite`, so the kit
|
|
300
|
+
adds those buckets once and preserves reported output and total. Reasoning is
|
|
301
|
+
already part of output and never added again. Missing/inconsistent usage stays
|
|
302
|
+
partial/unknown; engine cost estimates, raw answers and secrets are discarded.
|
|
303
|
+
`normalizeTokens(provider, result, 'openclaw')` exposes the same pure conversion.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { callLedger, memoryTokenLedgerStore, tokenLedger } from '@byokit/usage';
|
|
307
|
+
import type { RunEnd } from '@byokit/openclaw';
|
|
308
|
+
import type { AnthropicResult } from '@byokit/accounts';
|
|
309
|
+
import type { Answer } from '@byokit/decide';
|
|
310
|
+
|
|
311
|
+
const store = memoryTokenLedgerStore();
|
|
312
|
+
const calls = callLedger({ store });
|
|
313
|
+
// The app supplies its member policy, run identity and selected lane/route/model.
|
|
314
|
+
const limits = tokenLedger({ store, cap: (member) => member === 'member-one' ? 50_000 : undefined });
|
|
315
|
+
const context = {
|
|
316
|
+
provider: 'anthropic', account: 'non-secret-account-id', model: 'selected-model',
|
|
317
|
+
lane: 'host', route: 'anthropic-cli', runId: 'run-one',
|
|
318
|
+
};
|
|
319
|
+
declare const end: RunEnd; // Returned by the kit's existing run; no extra request.
|
|
320
|
+
if (end.ok) {
|
|
321
|
+
calls.record('member-one', { ...context, time: Date.now(), usage: end, usageFormat: 'openclaw' });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
declare const answer: Answer;
|
|
325
|
+
// Record once per actual backend invocation, not once per question or cache hit.
|
|
326
|
+
// The host selected this API-billed backend only after the person's opt-in.
|
|
327
|
+
if (answer.source === 'api') {
|
|
328
|
+
calls.record('member-one', { ...context, route: 'decision', billing: 'api',
|
|
329
|
+
time: Date.now(), usage: answer });
|
|
330
|
+
}
|
|
331
|
+
declare const messages: AnthropicResult;
|
|
332
|
+
// API key (billed per use); consent and the original request belong to the app.
|
|
333
|
+
calls.record('member-one', { ...context, route: 'anthropic', billing: 'api',
|
|
334
|
+
time: Date.now(), usage: messages });
|
|
335
|
+
|
|
336
|
+
declare const runStartedAt: number;
|
|
337
|
+
const run = calls.queryRun('member-one', 'run-one', runStartedAt, Date.now() + 1);
|
|
338
|
+
const history = calls.runs('member-one', runStartedAt, Date.now() + 1);
|
|
339
|
+
const allowance = limits.query('member-one', runStartedAt, Date.now() + 1);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The host records either an OpenClaw aggregate result or its individual calls, never
|
|
343
|
+
both. Decide can attach one invocation's usage to several answers; record it once,
|
|
344
|
+
and skip `source: 'cache'` answers. String-only accounts/answerer results carry no
|
|
345
|
+
counts and remain unknown; the ledger makes no recovery requests. Model, provider,
|
|
346
|
+
lane and route describe the actual execution and are supplied by the app; there is
|
|
347
|
+
no fallback to an API-billed route. Per-run counts stay on device, in memory by
|
|
348
|
+
default. A custom store must keep them on device and apply the app's retention
|
|
349
|
+
policy. The kit never logs counts, sends telemetry or stores the raw result.
|
|
350
|
+
Iteration budgets, stopping rules, consent and presentation belong to the app.
|
|
351
|
+
|
|
267
352
|
When passing normalized windows to `@byokit/accounts`' structural helper, use
|
|
268
353
|
`roomOf(reading.windows, reading.at, 'milliseconds')`. Its two-argument form is for
|
|
269
354
|
legacy reset seconds; normalized usage windows in 0.2.0+ already use milliseconds.
|
|
270
355
|
Alternatively, this package's `roomOf(reading, nowMs)` returns a structural `Room`
|
|
271
356
|
that the accounts chooser accepts directly. Preserve the original measurement time.
|
|
357
|
+
|
|
358
|
+
`identity(codexSource)` shares the app-server transport, calls `account/read` with a 15-second deadline, never opens a credential file, and returns only `{signedIn,email?,plan?}`. Managed-folder HTTP usage carries only the app-passed headers plus Bearer authorization and JSON accept; it uses the same bounded HTTP transport.
|
|
359
|
+
|
|
360
|
+
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
|
+
|
|
362
|
+
## React Native
|
|
363
|
+
|
|
364
|
+
The `react-native` condition of `@byokit/usage` selects a portable entry. The explicit
|
|
365
|
+
`@byokit/usage/react-native` subpath selects the same API when a bundler does not
|
|
366
|
+
use export conditions. It needs no native module, Node shim, credentials or network.
|
|
367
|
+
The default Node entry and browser resolution are unchanged.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { callLedger, tokenLedger, memoryTokenLedgerStore } from '@byokit/usage/react-native';
|
|
371
|
+
const store = memoryTokenLedgerStore(); // Replace with an app-owned synchronous durable store.
|
|
372
|
+
const calls = callLedger({ store });
|
|
373
|
+
const tokens = tokenLedger({ store, cap: 10_000 });
|
|
374
|
+
const time = Date.now();
|
|
375
|
+
calls.record('member-1', {
|
|
376
|
+
provider: 'openai', account: 'app-account', model: 'app-model', runId: 'run-1',
|
|
377
|
+
time, billing: 'api', lane: 'host-lane', route: 'host-route',
|
|
378
|
+
usage: { input_tokens: 12, output_tokens: 8 },
|
|
379
|
+
});
|
|
380
|
+
const daily = tokens.query('member-1', time, time + 1);
|
|
381
|
+
const history = calls.query('member-1', time, time + 1);
|
|
382
|
+
const runs = calls.runs('member-1', time, time + 1);
|
|
383
|
+
const run = calls.queryRun('member-1', 'run-1', time, time + 1);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
This entry exports `callLedger`, `tokenLedger`, `memoryTokenLedgerStore`,
|
|
387
|
+
`TokenLedgerError`, `normalizeTokens`, `priceCall`, all quota parsers listed above,
|
|
388
|
+
`codexHardLimit`, `roomOf` and the words helpers, with their corresponding types
|
|
389
|
+
(including `RunQuery`). `callLedger` supports the same `runs`/`queryRun` methods and
|
|
390
|
+
host-supplied lane/route attribution as the Node entry.
|
|
391
|
+
The app supplies provider usage and quota payloads; `usage()`, credential/file
|
|
392
|
+
adapters, `identity()`, fingerprints and disk quota stores remain Node-only.
|
|
393
|
+
|
|
394
|
+
Counts retain reported/partial/unknown provenance. Missing counts remain unknown;
|
|
395
|
+
unknown calls suppress a positive remaining allowance. Subscription and API key
|
|
396
|
+
(billed per use) calls retain their separate billing attribution. Cost is absent
|
|
397
|
+
unless a matching app-owned price table supplies an estimate, labelled
|
|
398
|
+
“Person's own plan” or “Person's API bill”; no provider prices are invented and a
|
|
399
|
+
subscription quota is never converted into an API charge.
|
|
400
|
+
|
|
401
|
+
The offline consumer fixture in `test/rn-fixture.ts` exercises the built package's
|
|
402
|
+
React Native export. After building, run
|
|
403
|
+
`BYOKIT_HERMES=/absolute/path/to/hermes sh scripts/test.sh 'packages/usage/test/react-native.test.ts'`
|
|
404
|
+
from the repository root to execute it in a Hermes CLI VM. Without that optional
|
|
405
|
+
binary, the same contract runs in a sandbox without Node globals; the Hermes check
|
|
406
|
+
is skipped. The standalone fixture uses the locked Expo Babel preset's `hermes-v0`
|
|
407
|
+
profile to lower classes for legacy Hermes CLI VMs. This is VM qualification, not
|
|
408
|
+
an Expo SDK runtime, emulator or native UI test.
|
package/dist/calls.d.ts
CHANGED
|
@@ -31,8 +31,14 @@ export interface CallInput {
|
|
|
31
31
|
account: string;
|
|
32
32
|
model: string;
|
|
33
33
|
runId: string;
|
|
34
|
+
/** App-selected host lane and runtime route; never inferred from a model id. */
|
|
35
|
+
lane?: string;
|
|
36
|
+
route?: string;
|
|
34
37
|
time: number;
|
|
35
|
-
|
|
38
|
+
/** Subscription by default; API key (billed per use) attribution is explicit. */
|
|
39
|
+
billing?: 'subscription' | 'api';
|
|
40
|
+
/** OpenClaw reports input separately from cache reads/writes. */
|
|
41
|
+
usageFormat?: 'provider' | 'openclaw';
|
|
36
42
|
/** Native provider usage/envelope, or normalized input/output/cachedInput/cacheWrite/total counts. */
|
|
37
43
|
usage?: unknown;
|
|
38
44
|
payer?: string;
|
|
@@ -40,7 +46,9 @@ export interface CallInput {
|
|
|
40
46
|
state?: 'completed' | 'cancelled' | 'failed';
|
|
41
47
|
limits?: readonly Window[];
|
|
42
48
|
}
|
|
43
|
-
export interface CallRecord extends Omit<CallInput, 'usage' | 'limits'> {
|
|
49
|
+
export interface CallRecord extends Omit<CallInput, 'usage' | 'limits' | 'billing' | 'usageFormat'> {
|
|
50
|
+
billing: 'subscription' | 'api';
|
|
51
|
+
billingLabel: "Person's own plan" | "Person's API bill";
|
|
44
52
|
tokens: NormalizedTokens;
|
|
45
53
|
state: 'completed' | 'cancelled' | 'failed';
|
|
46
54
|
cost?: CallCost;
|
|
@@ -56,9 +64,16 @@ export interface CallQuery {
|
|
|
56
64
|
export interface CallLedger {
|
|
57
65
|
record(member: string, call: CallInput): CallRecord;
|
|
58
66
|
query(member: string, from: number, to: number): CallQuery;
|
|
67
|
+
/** All runs with recorded calls in [from, to), in first-call order. */
|
|
68
|
+
runs(member: string, from: number, to: number): RunQuery[];
|
|
69
|
+
/** One member's run, restricted to [from, to). Empty runs have zero totals. */
|
|
70
|
+
queryRun(member: string, runId: string, from: number, to: number): RunQuery;
|
|
71
|
+
}
|
|
72
|
+
export interface RunQuery extends CallQuery {
|
|
73
|
+
runId: string;
|
|
59
74
|
}
|
|
60
75
|
/** Pure normalization of reported counts, never estimates from text or shared decision invocations. */
|
|
61
|
-
export declare function normalizeTokens(provider: string, raw: unknown): NormalizedTokens;
|
|
76
|
+
export declare function normalizeTokens(provider: string, raw: unknown, format?: 'provider' | 'openclaw'): NormalizedTokens;
|
|
62
77
|
/** App-supplied price estimates only, including explicit billing attribution. */
|
|
63
78
|
export declare function priceCall(tokens: NormalizedTokens, price: ModelPrice | undefined, billing: ModelPrice['billing']): CallCost | undefined;
|
|
64
79
|
/** Every recorded attempt is a call; the host records retries separately under its run id. */
|
package/dist/calls.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { record as isRecord } from "./windows.js";
|
|
2
2
|
import { memoryTokenLedgerStore, TokenLedgerError } from "./ledger.js";
|
|
3
|
-
import { safeWindows } from "./
|
|
3
|
+
import { safeWindows } from "./safe-windows.js";
|
|
4
4
|
const object = (value) => isRecord(value) ? value : {};
|
|
5
5
|
const count = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
6
6
|
const text = (value) => typeof value === 'string' && !!value && value.length <= 1024 && !/[\0\r\n]/.test(value);
|
|
@@ -20,11 +20,20 @@ function normalized(input, output, cachedInput, cacheWrite, total) {
|
|
|
20
20
|
provenance: input !== undefined && output !== undefined && derived !== undefined ? 'reported' : [input, output, cachedInput, cacheWrite, measured].some((v) => v !== undefined) ? 'partial' : 'unknown' };
|
|
21
21
|
}
|
|
22
22
|
/** Pure normalization of reported counts, never estimates from text or shared decision invocations. */
|
|
23
|
-
export function normalizeTokens(provider, raw) {
|
|
23
|
+
export function normalizeTokens(provider, raw, format = 'provider') {
|
|
24
24
|
const envelope = object(raw);
|
|
25
25
|
const usage = object(envelope.usage ?? envelope.usageMetadata ?? raw);
|
|
26
26
|
if (usage.provenance === 'unknown' || usage.provenance === 'estimated')
|
|
27
27
|
return { provenance: 'unknown' };
|
|
28
|
+
if (format === 'openclaw') {
|
|
29
|
+
if (!['input', 'output', 'cacheRead', 'cacheWrite', 'total'].some((key) => count(usage[key]) !== undefined))
|
|
30
|
+
return { provenance: 'unknown' };
|
|
31
|
+
const uncached = count(usage.input);
|
|
32
|
+
const cached = count(usage.cacheRead) ?? (usage.cacheRead === undefined ? 0 : undefined);
|
|
33
|
+
const written = count(usage.cacheWrite) ?? (usage.cacheWrite === undefined ? 0 : undefined);
|
|
34
|
+
const input = uncached !== undefined && cached !== undefined && written !== undefined ? sum(uncached, cached, written) : undefined;
|
|
35
|
+
return normalized(input, count(usage.output), cached, written, count(usage.total));
|
|
36
|
+
}
|
|
28
37
|
if (['input', 'output', 'total'].some((key) => key in usage))
|
|
29
38
|
return normalized(count(usage.input), count(usage.output), count(usage.cachedInput), count(usage.cacheWrite), count(usage.total));
|
|
30
39
|
if (provider === 'claude' || provider === 'anthropic') {
|
|
@@ -76,21 +85,37 @@ function aggregate(calls) {
|
|
|
76
85
|
const field = (key) => calls.every((call) => call.tokens[key] !== undefined) ? count(calls.reduce((total, call) => total + call.tokens[key], 0)) : undefined;
|
|
77
86
|
return normalized(field('input'), field('output'), field('cachedInput'), field('cacheWrite'), field('total'));
|
|
78
87
|
}
|
|
88
|
+
function summarize(calls) {
|
|
89
|
+
const costs = new Map();
|
|
90
|
+
for (const call of calls) {
|
|
91
|
+
if (!call.cost)
|
|
92
|
+
continue;
|
|
93
|
+
const key = `${call.cost.currency}\0${call.cost.billing}`;
|
|
94
|
+
const previous = costs.get(key);
|
|
95
|
+
costs.set(key, { ...call.cost, amount: (previous?.amount ?? 0) + call.cost.amount });
|
|
96
|
+
}
|
|
97
|
+
return { calls, tokens: aggregate(calls), costs: [...costs.values()], unpricedCalls: calls.filter((call) => !call.cost).length };
|
|
98
|
+
}
|
|
79
99
|
/** Every recorded attempt is a call; the host records retries separately under its run id. */
|
|
80
100
|
export function callLedger(options = {}) {
|
|
81
101
|
const store = options.store ?? memoryTokenLedgerStore();
|
|
82
|
-
|
|
102
|
+
const ledger = {
|
|
83
103
|
record(member, call) {
|
|
84
104
|
if (!text(member) || !call || ![call.provider, call.account, call.model, call.runId].every(text) || !time(call.time)
|
|
85
|
-
|| !['subscription', 'api'].includes(call.billing) || call.payer !== undefined && !text(call.payer)
|
|
105
|
+
|| call.billing !== undefined && !['subscription', 'api'].includes(call.billing) || call.payer !== undefined && !text(call.payer)
|
|
106
|
+
|| call.lane !== undefined && !text(call.lane) || call.route !== undefined && !text(call.route)
|
|
107
|
+
|| call.usageFormat !== undefined && !['provider', 'openclaw'].includes(call.usageFormat)
|
|
86
108
|
|| call.durationMs !== undefined && (typeof call.durationMs !== 'number' || !Number.isFinite(call.durationMs) || call.durationMs < 0)
|
|
87
109
|
|| call.state !== undefined && !['completed', 'cancelled', 'failed'].includes(call.state) || call.limits !== undefined && !Array.isArray(call.limits))
|
|
88
110
|
throw new TokenLedgerError('invalid');
|
|
89
|
-
const
|
|
90
|
-
const
|
|
111
|
+
const billing = call.billing ?? 'subscription';
|
|
112
|
+
const tokens = normalizeTokens(call.provider, call.usage, call.usageFormat);
|
|
113
|
+
const cost = priceCall(tokens, options.prices?.[call.provider]?.[call.model], billing);
|
|
91
114
|
const limits = call.limits?.flatMap((window) => safeWindows(window.provider, [window]));
|
|
92
115
|
const result = { provider: call.provider, account: call.account, model: call.model, runId: call.runId, time: call.time,
|
|
93
|
-
billing:
|
|
116
|
+
billing, billingLabel: billing === 'subscription' ? "Person's own plan" : "Person's API bill",
|
|
117
|
+
payer: call.payer ?? member, state: call.state ?? 'completed', tokens,
|
|
118
|
+
...(call.lane === undefined ? {} : { lane: call.lane }), ...(call.route === undefined ? {} : { route: call.route }),
|
|
94
119
|
...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }), ...(cost ? { cost } : {}), ...(limits ? { limits } : {}) };
|
|
95
120
|
try {
|
|
96
121
|
store.record(member, { time: call.time, tokens: tokens.total ?? 0, call: copyCall(result) });
|
|
@@ -113,19 +138,27 @@ export function callLedger(options = {}) {
|
|
|
113
138
|
if (!Array.isArray(entries))
|
|
114
139
|
throw new TokenLedgerError('invalid');
|
|
115
140
|
const calls = entries.filter((entry) => entry.call && entry.time >= from && entry.time < to).map((entry) => copyCall(entry.call)).sort((a, b) => a.time - b.time);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
141
|
+
return summarize(calls);
|
|
142
|
+
},
|
|
143
|
+
runs(member, from, to) {
|
|
144
|
+
const groups = new Map();
|
|
145
|
+
for (const call of ledger.query(member, from, to).calls) {
|
|
146
|
+
const calls = groups.get(call.runId) ?? [];
|
|
147
|
+
calls.push(call);
|
|
148
|
+
groups.set(call.runId, calls);
|
|
123
149
|
}
|
|
124
|
-
return
|
|
150
|
+
return [...groups].map(([runId, calls]) => ({ runId, ...summarize(calls) }));
|
|
151
|
+
},
|
|
152
|
+
queryRun(member, runId, from, to) {
|
|
153
|
+
if (!text(runId))
|
|
154
|
+
throw new TokenLedgerError('invalid');
|
|
155
|
+
return { runId, ...summarize(ledger.query(member, from, to).calls.filter((call) => call.runId === runId)) };
|
|
125
156
|
},
|
|
126
157
|
};
|
|
158
|
+
return ledger;
|
|
127
159
|
}
|
|
128
160
|
function copyCall(call) {
|
|
129
|
-
return { ...call,
|
|
161
|
+
return { ...call, billingLabel: call.billing === 'subscription' ? "Person's own plan" : "Person's API bill",
|
|
162
|
+
tokens: { ...call.tokens }, ...(call.cost ? { cost: { ...call.cost } } : {}),
|
|
130
163
|
...(call.limits ? { limits: call.limits.map((window) => ({ ...window })) } : {}) };
|
|
131
164
|
}
|
package/dist/claude.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { providerHttp, type Answer } from './providers.ts';
|
|
2
|
+
import type { Source } from './types.ts';
|
|
3
|
+
type ClaudeSource = Extract<Source, {
|
|
4
|
+
folder: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** No ambient HOME discovery. Both the lexical and real folder must be managed by this root. */
|
|
7
|
+
export declare function managedClaudeFolder(folder: string, stateDir: string): boolean;
|
|
8
|
+
/** Metadata only, so account/cache probes never load a token. */
|
|
9
|
+
export declare function claudeCredential(folder: string): import("fs").Stats | undefined;
|
|
10
|
+
/** The credential is held only for this request; no refresh, writes or recovery sidecars. */
|
|
11
|
+
export declare function claudeUsage(source: ClaudeSource, fetcher: typeof fetch, nowMs: number, pacing?: Parameters<typeof providerHttp>[5]): Promise<Answer>;
|
|
12
|
+
export {};
|
package/dist/claude.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { lstatSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, sep } from 'node:path';
|
|
3
|
+
import { readJson } from "./store.js";
|
|
4
|
+
import { record } from "./windows.js";
|
|
5
|
+
import { providerHttp } from "./providers.js";
|
|
6
|
+
/** No ambient HOME discovery. Both the lexical and real folder must be managed by this root. */
|
|
7
|
+
export function managedClaudeFolder(folder, stateDir) {
|
|
8
|
+
const root = resolve(stateDir);
|
|
9
|
+
const candidate = resolve(folder);
|
|
10
|
+
if (root.split(sep).some((part) => ['.claude', '.codex', '.pi'].includes(part)))
|
|
11
|
+
return false;
|
|
12
|
+
const parent = join(root, 'claude');
|
|
13
|
+
if (!candidate.startsWith(parent + sep) || !/^[a-f0-9]+$/.test(candidate.slice(parent.length + 1)))
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
if (![root, parent, candidate].every((path) => { const s = lstatSync(path); return s.isDirectory() && !s.isSymbolicLink(); }))
|
|
17
|
+
return false;
|
|
18
|
+
const realRoot = realpathSync(root);
|
|
19
|
+
if (realRoot.split(sep).some((part) => ['.claude', '.codex', '.pi'].includes(part)))
|
|
20
|
+
return false;
|
|
21
|
+
const realFolder = realpathSync(candidate);
|
|
22
|
+
return realFolder.startsWith(join(realRoot, 'claude') + sep);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Metadata only, so account/cache probes never load a token. */
|
|
29
|
+
export function claudeCredential(folder) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = lstatSync(join(folder, '.credentials.json'));
|
|
32
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.size <= 64 * 1024 ? stat : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The credential is held only for this request; no refresh, writes or recovery sidecars. */
|
|
39
|
+
export async function claudeUsage(source, fetcher, nowMs, pacing) {
|
|
40
|
+
const raw = readJson(join(source.folder, '.credentials.json'), 64 * 1024);
|
|
41
|
+
const oauth = record(raw) && record(raw.claudeAiOauth) ? raw.claudeAiOauth : undefined;
|
|
42
|
+
if (!oauth || typeof oauth.accessToken !== 'string' || !oauth.accessToken.trim() || oauth.accessToken.length > 16384 || /[\x00-\x20\x7f]/.test(oauth.accessToken))
|
|
43
|
+
return { code: 'not-connected' };
|
|
44
|
+
if (typeof oauth.expiresAt !== 'number' || !Number.isFinite(oauth.expiresAt) || oauth.expiresAt <= nowMs)
|
|
45
|
+
return { code: 'expired' };
|
|
46
|
+
return providerHttp('https://api.anthropic.com/api/oauth/usage', oauth.accessToken, fetcher, nowMs, { headers: { 'anthropic-beta': source.headers['anthropic-beta'], 'User-Agent': source.headers['User-Agent'] } }, pacing);
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Source } from './types.ts';
|
|
2
|
+
export type Identity = {
|
|
3
|
+
signedIn: boolean;
|
|
4
|
+
email?: string;
|
|
5
|
+
plan?: string;
|
|
6
|
+
};
|
|
7
|
+
/** Explicit identity fields only: never walk arbitrary token-bearing objects. */
|
|
8
|
+
export declare function publicIdentity(raw: unknown, signedIn: boolean): Identity;
|
|
9
|
+
export declare function codexIdentity(source: Extract<Source, {
|
|
10
|
+
bin: string;
|
|
11
|
+
}>): Promise<Identity>;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { codexRequest } from "./providers.js";
|
|
2
|
+
import { record } from "./windows.js";
|
|
3
|
+
/** Explicit identity fields only: never walk arbitrary token-bearing objects. */
|
|
4
|
+
export function publicIdentity(raw, signedIn) {
|
|
5
|
+
if (!signedIn || !record(raw))
|
|
6
|
+
return { signedIn: false };
|
|
7
|
+
const short = (value) => typeof value === 'string' && value.length > 0 && value.length <= 320 && !/[\x00-\x1f\x7f]/.test(value);
|
|
8
|
+
const email = short(raw.email) && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw.email) ? raw.email : undefined;
|
|
9
|
+
const plan = [raw.planType, raw.subscriptionType, raw.plan, raw.tier, raw.planName].find((value) => short(value) && /^[a-zA-Z][a-zA-Z0-9 _+-]{0,63}$/.test(value));
|
|
10
|
+
return { signedIn: true, ...(email === undefined ? {} : { email }), ...(typeof plan === 'string' ? { plan } : {}) };
|
|
11
|
+
}
|
|
12
|
+
export async function codexIdentity(source) {
|
|
13
|
+
const answer = await codexRequest(source, 'account/read');
|
|
14
|
+
const account = record(answer.raw) ? answer.raw.account : undefined;
|
|
15
|
+
return publicIdentity(account, record(account));
|
|
16
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { type Usage, type UsageOptions } from './types.ts';
|
|
1
|
+
import { type Source, type Usage, type UsageOptions } from './types.ts';
|
|
2
|
+
import { type Identity } from './identity.ts';
|
|
2
3
|
export * from './types.ts';
|
|
3
|
-
export
|
|
4
|
+
export type { Identity } from './identity.ts';
|
|
5
|
+
/** Identity runs only the named binary, never opens a credential file. */
|
|
6
|
+
export declare function identity(source: Extract<Source, {
|
|
7
|
+
bin: string;
|
|
8
|
+
}>): Promise<Identity>;
|
|
9
|
+
export { callLedger, normalizeTokens, priceCall, type CallLedger, type CallInput, type CallRecord, type CallQuery, type RunQuery, type NormalizedTokens, type ModelPrice, type PriceTable, type CallCost } from './calls.ts';
|
|
4
10
|
export { tokenLedger, memoryTokenLedgerStore, TokenLedgerError, type TokenLedger, type TokenLedgerStore, type TokenLedgerOptions, type TokenEntry, type TokenQuery } from './ledger.ts';
|
|
5
11
|
export { roomOf } from './room.ts';
|
|
6
12
|
export { fingerprint, store as fileUsageStore, memoryUsageStore } from './store.ts';
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,16 @@ import { fingerprint, readJson, store, memoryUsageStore, safeWindows, safePoll }
|
|
|
6
6
|
import { claudeWindows, codexWindows, goWindows, record, zaiWindows } from "./windows.js";
|
|
7
7
|
import { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from "./quota.js";
|
|
8
8
|
import { UsageError } from "./types.js";
|
|
9
|
+
import { codexIdentity } from "./identity.js";
|
|
10
|
+
import { claudeUsage as managedClaudeUsage, claudeCredential, managedClaudeFolder } from "./claude.js";
|
|
9
11
|
export * from "./types.js";
|
|
12
|
+
/** Identity runs only the named binary, never opens a credential file. */
|
|
13
|
+
export async function identity(source) {
|
|
14
|
+
if (!record(source) || !('bin' in source) || 'folder' in source || 'credentialsFile' in source || 'read' in source)
|
|
15
|
+
throw new UsageError();
|
|
16
|
+
validate(source);
|
|
17
|
+
return codexIdentity(source);
|
|
18
|
+
}
|
|
10
19
|
export { callLedger, normalizeTokens, priceCall } from "./calls.js";
|
|
11
20
|
export { tokenLedger, memoryTokenLedgerStore, TokenLedgerError } from "./ledger.js";
|
|
12
21
|
export { roomOf } from "./room.js";
|
|
@@ -17,10 +26,16 @@ export { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimax
|
|
|
17
26
|
export { WORDS, words, usageWords } from "./words.js";
|
|
18
27
|
const providers = ['claude', 'codex', 'opencode', 'zai', 'copilot', 'grok', 'minimax', 'gemini', 'kimi'];
|
|
19
28
|
const validText = (v) => typeof v === 'string' && !v.includes('\0') && !/[\r\n]/.test(v) && v.length <= 16384;
|
|
20
|
-
function validate(source) {
|
|
29
|
+
function validate(source, stateDir) {
|
|
21
30
|
if (!record(source) || !providers.includes(source.provider))
|
|
22
31
|
throw new UsageError();
|
|
23
|
-
if ('
|
|
32
|
+
if ('folder' in source) {
|
|
33
|
+
if (source.provider !== 'claude' || !validText(source.folder) || !isAbsolute(source.folder) || !stateDir || !managedClaudeFolder(source.folder, stateDir))
|
|
34
|
+
throw new UsageError();
|
|
35
|
+
if (!record(source.headers) || !['anthropic-beta', 'User-Agent'].every((key) => validText(source.headers[key]) && source.headers[key].length <= 1024))
|
|
36
|
+
throw new UsageError();
|
|
37
|
+
}
|
|
38
|
+
else if ('credentialsFile' in source) {
|
|
24
39
|
if (source.provider !== 'claude' || ![source.credentialsFile, ...[source.configFile, source.statuslineFile].filter((v) => v !== undefined)].every((v) => validText(v) && isAbsolute(v)))
|
|
25
40
|
throw new UsageError();
|
|
26
41
|
}
|
|
@@ -84,7 +99,9 @@ export function usage(options) {
|
|
|
84
99
|
const failures = new Map();
|
|
85
100
|
const now = (opts) => opts?.nowMs ?? (options.now ?? Date.now)();
|
|
86
101
|
function connected(source) {
|
|
87
|
-
validate(source);
|
|
102
|
+
validate(source, options.stateDir);
|
|
103
|
+
if ('folder' in source)
|
|
104
|
+
return claudeCredential(source.folder) !== undefined;
|
|
88
105
|
if ('credentialsFile' in source)
|
|
89
106
|
return claudeAuth(source) !== undefined;
|
|
90
107
|
if ('read' in source) {
|
|
@@ -107,9 +124,13 @@ export function usage(options) {
|
|
|
107
124
|
return ('access' in source ? source.access : source.key).trim() !== '';
|
|
108
125
|
}
|
|
109
126
|
function account(source) {
|
|
110
|
-
validate(source);
|
|
127
|
+
validate(source, options.stateDir);
|
|
111
128
|
let id;
|
|
112
|
-
if ('
|
|
129
|
+
if ('folder' in source) {
|
|
130
|
+
const stat = claudeCredential(source.folder);
|
|
131
|
+
id = stat ? `${source.folder}\0${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` : undefined;
|
|
132
|
+
}
|
|
133
|
+
else if ('credentialsFile' in source)
|
|
113
134
|
id = claudeAuth(source)?.account;
|
|
114
135
|
else if ('read' in source)
|
|
115
136
|
id = source.accountUuid;
|
|
@@ -164,7 +185,7 @@ export function usage(options) {
|
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
async function read(source, opts) {
|
|
167
|
-
validate(source);
|
|
188
|
+
validate(source, options.stateDir);
|
|
168
189
|
const clock = now(opts);
|
|
169
190
|
const empty = (code) => ({ provider: source.provider, windows: [], code, poll: { at: clock, outcome: code ?? 'unavailable' } });
|
|
170
191
|
if (!connected(source))
|
|
@@ -202,7 +223,7 @@ export function usage(options) {
|
|
|
202
223
|
let answer;
|
|
203
224
|
const pacing = { hook: options.pace, provider: source.provider, account: id.key, signal: opts?.signal };
|
|
204
225
|
try {
|
|
205
|
-
answer = 'read' in source ? await customClaude(source, clock, pacing) : 'credentialsFile' in source ? await claudeUsage(source, options.fetch ?? globalThis.fetch, clock, pacing)
|
|
226
|
+
answer = 'folder' in source ? await managedClaudeUsage(source, options.fetch ?? globalThis.fetch, clock, pacing) : 'read' in source ? await customClaude(source, clock, pacing) : 'credentialsFile' in source ? await claudeUsage(source, options.fetch ?? globalThis.fetch, clock, pacing)
|
|
206
227
|
: 'bin' in source ? await codexUsage(source) : await providerGet(source, options.fetch ?? globalThis.fetch, clock, pacing);
|
|
207
228
|
}
|
|
208
229
|
catch {
|
package/dist/providers.d.ts
CHANGED
|
@@ -5,6 +5,16 @@ type TokenSource = Extract<Source, {
|
|
|
5
5
|
} | {
|
|
6
6
|
key: string;
|
|
7
7
|
}>;
|
|
8
|
+
/** One bounded request; credentials and response bodies never become errors. */
|
|
9
|
+
export declare function providerHttp(url: string, key: string, fetcher: typeof fetch, nowMs: number, extra?: {
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
}, pacing?: {
|
|
13
|
+
hook?: PacingHook;
|
|
14
|
+
provider: Source['provider'];
|
|
15
|
+
account: string;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}): Promise<Answer>;
|
|
8
18
|
export declare function providerGet(source: TokenSource, fetcher: typeof fetch, nowMs: number, pacing?: {
|
|
9
19
|
hook?: PacingHook;
|
|
10
20
|
provider: Source['provider'];
|
|
@@ -22,6 +32,10 @@ export declare function customClaude(source: Extract<Source, {
|
|
|
22
32
|
export declare function codexUsage(source: Extract<Source, {
|
|
23
33
|
bin: string;
|
|
24
34
|
}>): Promise<Answer>;
|
|
35
|
+
/** The single bounded app-server transport used for identity and usage. */
|
|
36
|
+
export declare function codexRequest(source: Extract<Source, {
|
|
37
|
+
bin: string;
|
|
38
|
+
}>, method: 'account/read' | 'account/rateLimits/read', timeoutMs?: number): Promise<Answer>;
|
|
25
39
|
/** Re-read the tool's credentials so its own renewal is picked up; never renew or write them. */
|
|
26
40
|
export declare function claudeAuth(source: Extract<Source, {
|
|
27
41
|
credentialsFile: string;
|
package/dist/providers.js
CHANGED
|
@@ -5,7 +5,7 @@ import { claudeWindows, record } from "./windows.js";
|
|
|
5
5
|
import { grokWindows } from "./quota.js";
|
|
6
6
|
const USER_AGENT = 'byokit/usage/0.2.0';
|
|
7
7
|
/** One bounded request; credentials and response bodies never become errors. */
|
|
8
|
-
async function
|
|
8
|
+
export async function providerHttp(url, key, fetcher, nowMs, extra = {}, pacing) {
|
|
9
9
|
const controller = new AbortController();
|
|
10
10
|
const abort = () => controller.abort();
|
|
11
11
|
pacing?.signal?.addEventListener('abort', abort, { once: true });
|
|
@@ -69,7 +69,7 @@ async function request(url, key, fetcher, nowMs, extra = {}, pacing) {
|
|
|
69
69
|
}
|
|
70
70
|
export async function providerGet(source, fetcher, nowMs, pacing) {
|
|
71
71
|
const key = 'access' in source ? source.access : source.key;
|
|
72
|
-
const get = (url, extra) =>
|
|
72
|
+
const get = (url, extra) => providerHttp(url, key, fetcher, nowMs, extra, pacing);
|
|
73
73
|
switch (source.provider) {
|
|
74
74
|
case 'claude': return get('https://api.anthropic.com/api/oauth/usage', { headers: { 'anthropic-beta': 'oauth-2025-04-20' } });
|
|
75
75
|
case 'codex': return get('https://chatgpt.com/backend-api/wham/usage', { headers: { 'ChatGPT-Account-Id': source.accountId } });
|
|
@@ -138,6 +138,10 @@ export async function customClaude(source, nowMs, pacing) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
export function codexUsage(source) {
|
|
141
|
+
return codexRequest(source, 'account/rateLimits/read', 20_000);
|
|
142
|
+
}
|
|
143
|
+
/** The single bounded app-server transport used for identity and usage. */
|
|
144
|
+
export function codexRequest(source, method, timeoutMs = 15_000) {
|
|
141
145
|
return new Promise((resolve) => {
|
|
142
146
|
const child = spawn(source.bin, ['app-server'], { env: { ...source.env, CODEX_HOME: source.home }, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
143
147
|
let buffer = '';
|
|
@@ -156,7 +160,7 @@ export function codexUsage(source) {
|
|
|
156
160
|
}
|
|
157
161
|
resolve(answer);
|
|
158
162
|
};
|
|
159
|
-
const timer = setTimeout(() => finish({ code: 'unavailable' }),
|
|
163
|
+
const timer = setTimeout(() => finish({ code: 'unavailable' }), timeoutMs);
|
|
160
164
|
child.once('error', () => finish({ code: 'unavailable' }));
|
|
161
165
|
child.once('close', () => { clearTimeout(escalation); finish({ code: 'unavailable' }); });
|
|
162
166
|
child.stdin.on('error', () => finish({ code: 'unavailable' }));
|
|
@@ -181,7 +185,7 @@ export function codexUsage(source) {
|
|
|
181
185
|
if (!record(message))
|
|
182
186
|
continue;
|
|
183
187
|
if (message.id === 1)
|
|
184
|
-
child.stdin.write(`${JSON.stringify({ id: 2, method
|
|
188
|
+
child.stdin.write(`${JSON.stringify({ id: 2, method, params: {} })}\n`);
|
|
185
189
|
if (message.id === 2)
|
|
186
190
|
finish(record(message.result) ? { raw: message.result } : { code: 'incomplete' });
|
|
187
191
|
}
|
package/dist/rn.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Local usage accounting and host-supplied quota parsing; no credential or network access. */
|
|
2
|
+
export { callLedger, normalizeTokens, priceCall, type CallLedger, type CallInput, type CallRecord, type CallQuery, type RunQuery, type NormalizedTokens, type ModelPrice, type PriceTable, type CallCost } from './calls.ts';
|
|
3
|
+
export { tokenLedger, memoryTokenLedgerStore, TokenLedgerError, type TokenLedger, type TokenLedgerStore, type TokenLedgerOptions, type TokenEntry, type TokenQuery } from './ledger.ts';
|
|
4
|
+
export { roomOf } from './room.ts';
|
|
5
|
+
export { claudeWindows, codexWindows, goWindows, zaiWindows, type CodexRateLimitResult } from './windows.ts';
|
|
6
|
+
export { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from './quota.ts';
|
|
7
|
+
export type { Provider, Kind, Window, Scope, Poll, Freshness, Code, Reading, Room } from './types.ts';
|
|
8
|
+
export { WORDS, words, usageWords, type WordKey } from './words.ts';
|
package/dist/rn.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Local usage accounting and host-supplied quota parsing; no credential or network access. */
|
|
2
|
+
export { callLedger, normalizeTokens, priceCall } from "./calls.js";
|
|
3
|
+
export { tokenLedger, memoryTokenLedgerStore, TokenLedgerError } from "./ledger.js";
|
|
4
|
+
export { roomOf } from "./room.js";
|
|
5
|
+
export { claudeWindows, codexWindows, goWindows, zaiWindows } from "./windows.js";
|
|
6
|
+
export { codexHardLimit, codexTokenWindows, copilotWindows, grokWindows, minimaxWindows, geminiWindows, kimiWindows } from "./quota.js";
|
|
7
|
+
export { WORDS, words, usageWords } from "./words.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { record, quotaScope } from "./windows.js";
|
|
2
|
+
/** Public stores receive only these quota fields, never raw provider payloads. */
|
|
3
|
+
export function safeWindows(provider, raw) {
|
|
4
|
+
return (Array.isArray(raw) ? raw : []).slice(0, 64).flatMap((value) => {
|
|
5
|
+
if (!record(value) || !['session', 'weekly', 'monthly', 'rolling', 'custom'].includes(String(value.kind)) || value.usedPercent !== undefined && (typeof value.usedPercent !== 'number' || !Number.isFinite(value.usedPercent)))
|
|
6
|
+
return [];
|
|
7
|
+
return [{ provider, kind: value.kind, ...(typeof value.usedPercent === 'number' ? { usedPercent: Math.max(0, Math.min(100, value.usedPercent)) } : {}),
|
|
8
|
+
...(quotaScope(value.scope) ? { scope: quotaScope(value.scope) } : {}),
|
|
9
|
+
...(typeof value.minutes === 'number' && Number.isFinite(value.minutes) && value.minutes > 0 ? { minutes: value.minutes } : {}),
|
|
10
|
+
...(typeof value.resetsAt === 'number' && Number.isFinite(value.resetsAt) ? { resetsAt: value.resetsAt } : {}),
|
|
11
|
+
...(value.limited === true ? { limited: true } : {}),
|
|
12
|
+
...(typeof value.limit === 'string' && value.limit.length <= 80 ? { limit: value.limit } : {}) }];
|
|
13
|
+
});
|
|
14
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Provider, StoredReading, UsageStore,
|
|
1
|
+
import type { Provider, StoredReading, UsageStore, Poll } from './types.ts';
|
|
2
|
+
export { safeWindows } from './safe-windows.ts';
|
|
2
3
|
export type Stored = StoredReading;
|
|
3
4
|
/** Bounded regular files only; do not follow credential or state symlinks. */
|
|
4
5
|
export declare function readJsonSnapshot(file: string, cap: number): {
|
|
@@ -10,6 +11,4 @@ export declare function fingerprint(salt: string): (provider: Provider, value: s
|
|
|
10
11
|
/** Poll metadata is distinct from the observation timestamp. */
|
|
11
12
|
export declare function safePoll(raw: unknown): Poll | undefined;
|
|
12
13
|
export declare function store(stateDir: string): UsageStore;
|
|
13
|
-
/** Public stores receive only these quota fields, never raw provider payloads. */
|
|
14
|
-
export declare function safeWindows(provider: Provider, raw: unknown): Window[];
|
|
15
14
|
export declare function memoryUsageStore(): UsageStore;
|
package/dist/store.js
CHANGED
|
@@ -2,7 +2,9 @@ import { createHash, randomUUID, scryptSync } from 'node:crypto';
|
|
|
2
2
|
import { chmodSync, closeSync, fstatSync, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeFileSync, constants } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join } from 'node:path';
|
|
4
4
|
import { UsageError } from "./types.js";
|
|
5
|
-
import { record
|
|
5
|
+
import { record } from "./windows.js";
|
|
6
|
+
import { safeWindows } from "./safe-windows.js";
|
|
7
|
+
export { safeWindows } from "./safe-windows.js";
|
|
6
8
|
/** Bounded regular files only; do not follow credential or state symlinks. */
|
|
7
9
|
export function readJsonSnapshot(file, cap) {
|
|
8
10
|
let fd;
|
|
@@ -103,19 +105,6 @@ export function store(stateDir) {
|
|
|
103
105
|
},
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
|
-
/** Public stores receive only these quota fields, never raw provider payloads. */
|
|
107
|
-
export function safeWindows(provider, raw) {
|
|
108
|
-
return (Array.isArray(raw) ? raw : []).slice(0, 64).flatMap((value) => {
|
|
109
|
-
if (!record(value) || !['session', 'weekly', 'monthly', 'rolling', 'custom'].includes(String(value.kind)) || value.usedPercent !== undefined && (typeof value.usedPercent !== 'number' || !Number.isFinite(value.usedPercent)))
|
|
110
|
-
return [];
|
|
111
|
-
return [{ provider, kind: value.kind, ...(typeof value.usedPercent === 'number' ? { usedPercent: Math.max(0, Math.min(100, value.usedPercent)) } : {}),
|
|
112
|
-
...(quotaScope(value.scope) ? { scope: quotaScope(value.scope) } : {}),
|
|
113
|
-
...(typeof value.minutes === 'number' && Number.isFinite(value.minutes) && value.minutes > 0 ? { minutes: value.minutes } : {}),
|
|
114
|
-
...(typeof value.resetsAt === 'number' && Number.isFinite(value.resetsAt) ? { resetsAt: value.resetsAt } : {}),
|
|
115
|
-
...(value.limited === true ? { limited: true } : {}),
|
|
116
|
-
...(typeof value.limit === 'string' && value.limit.length <= 80 ? { limit: value.limit } : {}) }];
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
108
|
export function memoryUsageStore() {
|
|
120
109
|
const readings = new Map();
|
|
121
110
|
return { get: (provider, account) => { const r = readings.get(`${provider}\0${account}`); return r ? { at: r.at, windows: safeWindows(provider, r.windows), ...(r.limited ? { limited: true } : {}), ...(safePoll(r.poll) ? { poll: safePoll(r.poll) } : {}) } : undefined; },
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Read subscription usage windows per provider and account.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,12 +14,20 @@
|
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
|
+
"react-native": {
|
|
18
|
+
"types": "./dist/rn.d.ts",
|
|
19
|
+
"default": "./dist/rn.js"
|
|
20
|
+
},
|
|
17
21
|
"types": "./dist/index.d.ts",
|
|
18
22
|
"default": "./dist/index.js"
|
|
19
23
|
},
|
|
20
24
|
"./testing": {
|
|
21
25
|
"types": "./dist/testing/index.d.ts",
|
|
22
26
|
"default": "./dist/testing/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./react-native": {
|
|
29
|
+
"types": "./dist/rn.d.ts",
|
|
30
|
+
"default": "./dist/rn.js"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"files": [
|