@usebillow/sdk 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +274 -0
  4. package/dist/billing-C4RIMgH_.d.ts +1053 -0
  5. package/dist/billing-DZ4rIyg7.d.cts +1053 -0
  6. package/dist/billing-status-BZQN_gm7.d.cts +29 -0
  7. package/dist/billing-status-BZQN_gm7.d.ts +29 -0
  8. package/dist/chunk-CCG4F5FK.js +48 -0
  9. package/dist/chunk-CCG4F5FK.js.map +1 -0
  10. package/dist/chunk-Z6VXPONT.js +1493 -0
  11. package/dist/chunk-Z6VXPONT.js.map +1 -0
  12. package/dist/config.cjs +233 -0
  13. package/dist/config.cjs.map +1 -0
  14. package/dist/config.d.cts +104 -0
  15. package/dist/config.d.ts +104 -0
  16. package/dist/config.js +228 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/credits-C3Fe3TO0.d.cts +315 -0
  19. package/dist/credits-C3Fe3TO0.d.ts +315 -0
  20. package/dist/index.cjs +1560 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.cts +2379 -0
  23. package/dist/index.d.ts +2379 -0
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ingestion.cjs +259 -0
  27. package/dist/ingestion.cjs.map +1 -0
  28. package/dist/ingestion.d.cts +182 -0
  29. package/dist/ingestion.d.ts +182 -0
  30. package/dist/ingestion.js +252 -0
  31. package/dist/ingestion.js.map +1 -0
  32. package/dist/react.cjs +360 -0
  33. package/dist/react.cjs.map +1 -0
  34. package/dist/react.d.cts +71 -0
  35. package/dist/react.d.ts +71 -0
  36. package/dist/react.js +153 -0
  37. package/dist/react.js.map +1 -0
  38. package/dist/server.cjs +98 -0
  39. package/dist/server.cjs.map +1 -0
  40. package/dist/server.d.cts +54 -0
  41. package/dist/server.d.ts +54 -0
  42. package/dist/server.js +96 -0
  43. package/dist/server.js.map +1 -0
  44. package/dist/status.cjs +60 -0
  45. package/dist/status.cjs.map +1 -0
  46. package/dist/status.d.cts +31 -0
  47. package/dist/status.d.ts +31 -0
  48. package/dist/status.js +3 -0
  49. package/dist/status.js.map +1 -0
  50. package/dist/webhooks.cjs +157 -0
  51. package/dist/webhooks.cjs.map +1 -0
  52. package/dist/webhooks.d.cts +391 -0
  53. package/dist/webhooks.d.ts +391 -0
  54. package/dist/webhooks.js +143 -0
  55. package/dist/webhooks.js.map +1 -0
  56. package/package.json +169 -0
@@ -0,0 +1,182 @@
1
+ import { Billow } from './index.js';
2
+ import './billing-C4RIMgH_.js';
3
+ import 'zod';
4
+ import './billing-status-BZQN_gm7.js';
5
+ import './status.js';
6
+ import './credits-C3Fe3TO0.js';
7
+
8
+ /**
9
+ * Auto-metering (PRD-16): ingestion strategies that call billow's `track()` for you,
10
+ * so you never hand-instrument usage.
11
+ *
12
+ * Two layers:
13
+ * 1. A generic primitive — {@link createUsageMeter} + {@link meterFunction} — a
14
+ * non-blocking, buffered, idempotent dispatcher you can point at ANY usage
15
+ * source (an S3 upload, a job run, a DB write).
16
+ * 2. LLM strategies built on it — {@link meterOpenAI} (OpenAI-compatible clients)
17
+ * and {@link meterAIResult} (Vercel AI SDK) that auto-count prompt+completion
18
+ * tokens from the provider's own response (never re-tokenizing).
19
+ *
20
+ * Posture (every strategy): SERVER-SIDE ONLY — a browser can't be trusted to
21
+ * self-report usage — and NON-BLOCKING: metering is a side effect that can never
22
+ * delay or break the wrapped call. A failed `track()` buffers and retries under an
23
+ * idempotency key; if it ultimately fails it is dropped (via `onError`), never thrown.
24
+ *
25
+ * import { Billow } from "@usebillow/sdk";
26
+ * import { createUsageMeter, meterOpenAI } from "@usebillow/sdk/ingestion";
27
+ *
28
+ * const billow = new Billow(process.env.BILLOW_SECRET_KEY!);
29
+ * const meter = createUsageMeter(billow);
30
+ * const openai = meterOpenAI(new OpenAI(), {
31
+ * meter,
32
+ * feature: "tokens",
33
+ * customer: (params) => String(params.user), // your billow customer id
34
+ * });
35
+ * // use `openai` exactly as before — every completion auto-meters its tokens.
36
+ */
37
+
38
+ /** One usage event handed to `track()`. Mirrors {@link Billow.track}'s input. */
39
+ interface UsageRecord {
40
+ customerId: string;
41
+ featureId: string;
42
+ /** Units to record (the engine defaults to 1). */
43
+ value?: number;
44
+ /** Event properties for meter filters / property-based aggregations. */
45
+ properties?: Record<string, string | number>;
46
+ /**
47
+ * Dedup key. Auto-generated per record when omitted, so a retried dispatch is a
48
+ * no-op — set it yourself to dedupe across process restarts.
49
+ */
50
+ idempotencyKey?: string;
51
+ }
52
+ /** The minimal client the meter needs — the real {@link Billow} satisfies it. */
53
+ type TrackClient = Pick<Billow, "track">;
54
+ interface MeterOptions {
55
+ /** Max dispatch attempts for one record before it's dropped (default 5). */
56
+ maxAttempts?: number;
57
+ /** Base retry backoff in ms; doubles each attempt (default 250). */
58
+ retryBaseMs?: number;
59
+ /** Called once when a record is permanently dropped after exhausting retries. */
60
+ onError?: (error: unknown, record: UsageRecord) => void;
61
+ }
62
+ /**
63
+ * A buffered, non-blocking dispatcher to `track()`. `record()` returns immediately
64
+ * and never throws; delivery (with retries under an idempotency key) happens in the
65
+ * background. Call {@link UsageMeter.flush} before a serverless handler returns so
66
+ * pending records aren't lost when the process freezes.
67
+ */
68
+ interface UsageMeter {
69
+ /** Enqueue a usage event. Returns immediately, never throws. */
70
+ record(record: UsageRecord): void;
71
+ /**
72
+ * Enqueue a usage event whose record resolves LATER (e.g. a streamed response
73
+ * whose token totals arrive as a promise). Registers the pending work immediately
74
+ * so {@link UsageMeter.flush} awaits it — a `null` resolution or a rejected source
75
+ * is dropped. Returns immediately, never throws.
76
+ */
77
+ recordAsync(source: Promise<UsageRecord | null>): void;
78
+ /** Resolve once every buffered dispatch has settled (delivered or dropped). */
79
+ flush(): Promise<void>;
80
+ /** Records still in flight (queued or retrying). */
81
+ readonly pending: number;
82
+ }
83
+ /** Build a {@link UsageMeter} bound to a billow client (or any `{ track }`). */
84
+ declare function createUsageMeter(client: TrackClient, options?: MeterOptions): UsageMeter;
85
+ /**
86
+ * Derive a usage record from a completed call. Return `null` to skip metering it
87
+ * (e.g. a cache hit with no billable usage). Receives the resolved result and the
88
+ * original call arguments.
89
+ */
90
+ type UsageExtractor<A extends unknown[], R> = (result: R, args: A) => UsageRecord | null;
91
+ /**
92
+ * Wrap any async function so each successful call auto-records usage via `meter` —
93
+ * the generic ingestion primitive. Point it at any source by supplying an `extract`.
94
+ * Metering runs AFTER the call resolves and never blocks it; an extractor that
95
+ * throws is swallowed. If the wrapped call itself rejects, nothing is recorded and
96
+ * the error propagates unchanged.
97
+ */
98
+ declare function meterFunction<A extends unknown[], R>(fn: (...args: A) => Promise<R>, meter: UsageMeter, extract: UsageExtractor<A, R>): (...args: A) => Promise<R>;
99
+ /**
100
+ * Yield every value of `source` straight through to the consumer while calling
101
+ * `onValue` for each and `onDone` when iteration finishes (including an early
102
+ * `break`/`return` — it runs in a `finally`). Taps a stream WITHOUT consuming it:
103
+ * the caller still receives every chunk. Tap callbacks that throw are swallowed so
104
+ * they can't break the stream.
105
+ */
106
+ declare function tapAsyncIterable<T>(source: AsyncIterable<T>, taps: {
107
+ onValue?: (value: T) => void;
108
+ onDone?: () => void;
109
+ }): AsyncGenerator<T>;
110
+ /** Normalized token counts, whatever the provider's field naming. */
111
+ interface TokenCounts {
112
+ promptTokens: number;
113
+ completionTokens: number;
114
+ totalTokens: number;
115
+ }
116
+ /** Which token count a meter records as its `value` (default `"total"`). */
117
+ type TokenCount = "total" | "prompt" | "completion";
118
+ /**
119
+ * Read token counts from a provider usage object, accepting OpenAI (`prompt_tokens`
120
+ * / `completion_tokens` / `total_tokens`), Vercel AI SDK v4 (`promptTokens` / …) and
121
+ * v5 (`inputTokens` / `outputTokens`) shapes. Returns `null` when no token fields
122
+ * are present. `totalTokens` is derived from prompt+completion when not supplied.
123
+ */
124
+ declare function normalizeTokenUsage(usage: unknown): TokenCounts | null;
125
+ /** A value supplied directly, or resolved from a call's params. */
126
+ type Resolvable<T, P> = T | ((params: P) => T);
127
+ interface MeterLLMOptions<P = unknown> {
128
+ /** The buffered meter that dispatches to `track()`. */
129
+ meter: UsageMeter;
130
+ /** The metered feature (token meter) id — static, or resolved per call. */
131
+ feature: Resolvable<string, P>;
132
+ /**
133
+ * The billow customer to bill. Static for single-tenant, or resolved per call
134
+ * (e.g. from the OpenAI `user` param) for multi-tenant. Returning `null`/`undefined`
135
+ * skips metering that call — there's no customer to attribute it to.
136
+ */
137
+ customer: Resolvable<string | null | undefined, P>;
138
+ /** Which token count to record as the meter value (default `"total"`). */
139
+ count?: TokenCount;
140
+ /** Extra event properties merged onto each record (`model` is added automatically). */
141
+ properties?: (params: P, tokens: TokenCounts) => Record<string, string | number>;
142
+ }
143
+ /**
144
+ * Wrap an OpenAI-compatible client so every `chat.completions.create` call
145
+ * auto-meters its token usage. Returns a proxy of the SAME type — use it exactly as
146
+ * before; every other property/method passes straight through untouched.
147
+ *
148
+ * Reads the token count from the provider's own response (no re-tokenizing), for
149
+ * both blocking and streaming calls. For a STREAMED call, pass
150
+ * `stream_options: { include_usage: true }` so the final chunk carries usage —
151
+ * otherwise a streamed call can't be metered (billow never mutates your request).
152
+ *
153
+ * A streamed response is metered when consumed the standard way: `await` the call,
154
+ * then `for await` the stream. The `Stream` helpers (`.tee()`, `.toReadableStream()`)
155
+ * and the un-awaited `APIPromise` helpers (`.withResponse()`, `.asResponse()`) are
156
+ * PRESERVED (they keep working), but consuming through them bypasses the iteration
157
+ * tap, so those calls are not token-metered.
158
+ *
159
+ * Works with any client exposing `chat.completions.create` returning a `.usage`
160
+ * shape: OpenAI, Azure OpenAI, Together, Groq, OpenRouter, and other compatibles.
161
+ *
162
+ * const openai = meterOpenAI(new OpenAI(), { meter, feature: "tokens", customer });
163
+ * const res = await openai.chat.completions.create({ model, messages }); // metered
164
+ */
165
+ declare function meterOpenAI<T extends object>(client: T, options: MeterLLMOptions<Record<string, unknown>>): T;
166
+ /**
167
+ * Meter a Vercel AI SDK result (the object `generateText`/`streamText` resolves to).
168
+ * Reads `result.usage` — an object (`generateText`) or a Promise (`streamText`) — and
169
+ * records the token count, non-blocking. Never touches `result.textStream`, so a
170
+ * streamed response is delivered to your consumer untouched. Accepts AI SDK v4
171
+ * (`promptTokens`/`completionTokens`) and v5 (`inputTokens`/`outputTokens`) shapes.
172
+ *
173
+ * const result = streamText({ model, prompt });
174
+ * meterAIResult(result, { meter, feature: "tokens", customer: customerId });
175
+ * return result.toDataStreamResponse();
176
+ */
177
+ declare function meterAIResult(result: {
178
+ usage?: unknown;
179
+ response?: unknown;
180
+ }, options: MeterLLMOptions<void>): void;
181
+
182
+ export { type MeterLLMOptions, type MeterOptions, type TokenCount, type TokenCounts, type TrackClient, type UsageExtractor, type UsageMeter, type UsageRecord, createUsageMeter, meterAIResult, meterFunction, meterOpenAI, normalizeTokenUsage, tapAsyncIterable };
@@ -0,0 +1,252 @@
1
+ // src/ingestion.ts
2
+ var DEFAULT_MAX_ATTEMPTS = 5;
3
+ var DEFAULT_RETRY_BASE_MS = 250;
4
+ function newIdempotencyKey() {
5
+ return crypto.randomUUID();
6
+ }
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ function createUsageMeter(client, options = {}) {
11
+ const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
12
+ const retryBaseMs = Math.max(0, options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS);
13
+ const inFlight = /* @__PURE__ */ new Set();
14
+ async function dispatch(record) {
15
+ const input = {
16
+ ...record,
17
+ idempotencyKey: record.idempotencyKey ?? newIdempotencyKey()
18
+ };
19
+ let lastError;
20
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
21
+ try {
22
+ await client.track(input);
23
+ return;
24
+ } catch (error) {
25
+ lastError = error;
26
+ if (attempt < maxAttempts) await sleep(retryBaseMs * 2 ** (attempt - 1));
27
+ }
28
+ }
29
+ try {
30
+ options.onError?.(lastError, input);
31
+ } catch {
32
+ }
33
+ }
34
+ function track(work) {
35
+ const p = work.finally(() => {
36
+ inFlight.delete(p);
37
+ });
38
+ inFlight.add(p);
39
+ }
40
+ return {
41
+ record(record) {
42
+ track(dispatch(record));
43
+ },
44
+ recordAsync(source) {
45
+ track(
46
+ source.then(
47
+ (record) => record ? dispatch(record) : void 0,
48
+ () => void 0
49
+ )
50
+ );
51
+ },
52
+ flush() {
53
+ return Promise.all([...inFlight]).then(() => void 0);
54
+ },
55
+ get pending() {
56
+ return inFlight.size;
57
+ }
58
+ };
59
+ }
60
+ function meterFunction(fn, meter, extract) {
61
+ return async (...args) => {
62
+ const result = await fn(...args);
63
+ try {
64
+ const record = extract(result, args);
65
+ if (record) meter.record(record);
66
+ } catch {
67
+ }
68
+ return result;
69
+ };
70
+ }
71
+ async function* tapAsyncIterable(source, taps) {
72
+ try {
73
+ for await (const value of source) {
74
+ try {
75
+ taps.onValue?.(value);
76
+ } catch {
77
+ }
78
+ yield value;
79
+ }
80
+ } finally {
81
+ try {
82
+ taps.onDone?.();
83
+ } catch {
84
+ }
85
+ }
86
+ }
87
+ function firstNumber(...values) {
88
+ for (const v of values) if (typeof v === "number" && Number.isFinite(v)) return v;
89
+ return null;
90
+ }
91
+ function normalizeTokenUsage(usage) {
92
+ if (typeof usage !== "object" || usage === null) return null;
93
+ const u = usage;
94
+ const prompt = firstNumber(u.prompt_tokens, u.promptTokens, u.inputTokens);
95
+ const completion = firstNumber(u.completion_tokens, u.completionTokens, u.outputTokens);
96
+ const total = firstNumber(u.total_tokens, u.totalTokens);
97
+ if (prompt === null && completion === null && total === null) return null;
98
+ const promptTokens = prompt ?? 0;
99
+ const completionTokens = completion ?? 0;
100
+ return {
101
+ promptTokens,
102
+ completionTokens,
103
+ totalTokens: total ?? promptTokens + completionTokens
104
+ };
105
+ }
106
+ function pickCount(counts, which) {
107
+ if (which === "prompt") return counts.promptTokens;
108
+ if (which === "completion") return counts.completionTokens;
109
+ return counts.totalTokens;
110
+ }
111
+ function resolveValue(r, params) {
112
+ return typeof r === "function" ? r(params) : r;
113
+ }
114
+ function buildTokenRecord(options, params, usage, model) {
115
+ const counts = normalizeTokenUsage(usage);
116
+ if (!counts) return null;
117
+ try {
118
+ const customerId = resolveValue(options.customer, params);
119
+ if (!customerId) return null;
120
+ const properties = {
121
+ tokens_prompt: counts.promptTokens,
122
+ tokens_completion: counts.completionTokens,
123
+ tokens_total: counts.totalTokens,
124
+ ...typeof model === "string" ? { model } : {},
125
+ ...options.properties?.(params, counts)
126
+ };
127
+ return {
128
+ customerId,
129
+ featureId: resolveValue(options.feature, params),
130
+ value: pickCount(counts, options.count ?? "total"),
131
+ properties
132
+ };
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ function isPromiseLike(v) {
138
+ return (typeof v === "object" || typeof v === "function") && v !== null && typeof v.then === "function";
139
+ }
140
+ function wrapOpenAICreate(create, thisArg, options) {
141
+ return (params, ...rest) => {
142
+ const returned = Reflect.apply(create, thisArg, [params, ...rest]);
143
+ if (!isPromiseLike(returned)) return returned;
144
+ if (params.stream === true) {
145
+ return proxyResolved(
146
+ returned,
147
+ (stream) => typeof stream === "object" && stream !== null ? proxyAsyncIterable(
148
+ stream,
149
+ () => tapOpenAIStream(stream, params, options)
150
+ ) : stream
151
+ );
152
+ }
153
+ options.meter.recordAsync(returned.then((resp) => completionRecord(resp, params, options)));
154
+ return returned;
155
+ };
156
+ }
157
+ function completionRecord(resp, params, options) {
158
+ if (typeof resp !== "object" || resp === null) return null;
159
+ const r = resp;
160
+ return buildTokenRecord(options, params, r.usage, r.model);
161
+ }
162
+ function proxyResolved(source, map) {
163
+ const mappedThen = (onF, onR) => source.then((v) => onF ? onF(map(v)) : map(v), onR);
164
+ return new Proxy(source, {
165
+ get(target, prop) {
166
+ if (prop === "then") return mappedThen;
167
+ if (prop === "catch") return (onR) => mappedThen(void 0, onR);
168
+ if (prop === "finally") {
169
+ return (cb) => mappedThen(
170
+ (v) => {
171
+ cb?.();
172
+ return v;
173
+ },
174
+ (e) => {
175
+ cb?.();
176
+ throw e;
177
+ }
178
+ );
179
+ }
180
+ const value = Reflect.get(target, prop);
181
+ return typeof value === "function" ? value.bind(target) : value;
182
+ }
183
+ });
184
+ }
185
+ function proxyAsyncIterable(source, makeIterator) {
186
+ return new Proxy(source, {
187
+ get(target, prop) {
188
+ if (prop === Symbol.asyncIterator) return makeIterator;
189
+ const value = Reflect.get(target, prop);
190
+ return typeof value === "function" ? value.bind(target) : value;
191
+ }
192
+ });
193
+ }
194
+ function meterStream(stream, params, options, reduce) {
195
+ const acc = {};
196
+ return tapAsyncIterable(stream, {
197
+ onValue: (chunk) => reduce(chunk, acc),
198
+ onDone: () => {
199
+ const record = buildTokenRecord(options, params, acc.usage, acc.model);
200
+ if (record) options.meter.record(record);
201
+ }
202
+ });
203
+ }
204
+ function tapOpenAIStream(stream, params, options) {
205
+ return meterStream(stream, params, options, (chunk, acc) => {
206
+ if (typeof chunk !== "object" || chunk === null) return;
207
+ const c = chunk;
208
+ if (c.usage) acc.usage = c.usage;
209
+ if (c.model && acc.model === void 0) acc.model = c.model;
210
+ });
211
+ }
212
+ function meterOpenAI(client, options) {
213
+ const root = client;
214
+ const completions = root.chat?.completions;
215
+ const create = completions?.create;
216
+ if (typeof create !== "function" || !completions) {
217
+ throw new TypeError("meterOpenAI: expected client.chat.completions.create to be a function");
218
+ }
219
+ const wrapped = wrapOpenAICreate(create, completions, options);
220
+ return replaceCreate(client, wrapped);
221
+ }
222
+ function replaceCreate(client, wrapped) {
223
+ const root = client;
224
+ const completionsProxy = new Proxy(root.chat.completions, {
225
+ get: (t, p, r) => p === "create" ? wrapped : Reflect.get(t, p, r)
226
+ });
227
+ const chatProxy = new Proxy(root.chat, {
228
+ get: (t, p, r) => p === "completions" ? completionsProxy : Reflect.get(t, p, r)
229
+ });
230
+ return new Proxy(client, {
231
+ get: (t, p, r) => p === "chat" ? chatProxy : Reflect.get(t, p, r)
232
+ });
233
+ }
234
+ function meterAIResult(result, options) {
235
+ const model = extractAIModel(result.response);
236
+ const usage = result.usage;
237
+ if (isPromiseLike(usage)) {
238
+ options.meter.recordAsync(usage.then((u) => buildTokenRecord(options, void 0, u, model)));
239
+ return;
240
+ }
241
+ const record = buildTokenRecord(options, void 0, usage, model);
242
+ if (record) options.meter.record(record);
243
+ }
244
+ function extractAIModel(response) {
245
+ if (typeof response !== "object" || response === null) return void 0;
246
+ const m = response.modelId;
247
+ return typeof m === "string" ? m : void 0;
248
+ }
249
+
250
+ export { createUsageMeter, meterAIResult, meterFunction, meterOpenAI, normalizeTokenUsage, tapAsyncIterable };
251
+ //# sourceMappingURL=ingestion.js.map
252
+ //# sourceMappingURL=ingestion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ingestion.ts"],"names":[],"mappings":"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","file":"ingestion.js","sourcesContent":["/**\n * Auto-metering (PRD-16): ingestion strategies that call billow's `track()` for you,\n * so you never hand-instrument usage.\n *\n * Two layers:\n * 1. A generic primitive — {@link createUsageMeter} + {@link meterFunction} — a\n * non-blocking, buffered, idempotent dispatcher you can point at ANY usage\n * source (an S3 upload, a job run, a DB write).\n * 2. LLM strategies built on it — {@link meterOpenAI} (OpenAI-compatible clients)\n * and {@link meterAIResult} (Vercel AI SDK) that auto-count prompt+completion\n * tokens from the provider's own response (never re-tokenizing).\n *\n * Posture (every strategy): SERVER-SIDE ONLY — a browser can't be trusted to\n * self-report usage — and NON-BLOCKING: metering is a side effect that can never\n * delay or break the wrapped call. A failed `track()` buffers and retries under an\n * idempotency key; if it ultimately fails it is dropped (via `onError`), never thrown.\n *\n * import { Billow } from \"@usebillow/sdk\";\n * import { createUsageMeter, meterOpenAI } from \"@usebillow/sdk/ingestion\";\n *\n * const billow = new Billow(process.env.BILLOW_SECRET_KEY!);\n * const meter = createUsageMeter(billow);\n * const openai = meterOpenAI(new OpenAI(), {\n * meter,\n * feature: \"tokens\",\n * customer: (params) => String(params.user), // your billow customer id\n * });\n * // use `openai` exactly as before — every completion auto-meters its tokens.\n */\n\nimport type { Billow } from \"./index\";\n\n/** One usage event handed to `track()`. Mirrors {@link Billow.track}'s input. */\nexport interface UsageRecord {\n customerId: string;\n featureId: string;\n /** Units to record (the engine defaults to 1). */\n value?: number;\n /** Event properties for meter filters / property-based aggregations. */\n properties?: Record<string, string | number>;\n /**\n * Dedup key. Auto-generated per record when omitted, so a retried dispatch is a\n * no-op — set it yourself to dedupe across process restarts.\n */\n idempotencyKey?: string;\n}\n\n/** The minimal client the meter needs — the real {@link Billow} satisfies it. */\nexport type TrackClient = Pick<Billow, \"track\">;\n\nexport interface MeterOptions {\n /** Max dispatch attempts for one record before it's dropped (default 5). */\n maxAttempts?: number;\n /** Base retry backoff in ms; doubles each attempt (default 250). */\n retryBaseMs?: number;\n /** Called once when a record is permanently dropped after exhausting retries. */\n onError?: (error: unknown, record: UsageRecord) => void;\n}\n\n/**\n * A buffered, non-blocking dispatcher to `track()`. `record()` returns immediately\n * and never throws; delivery (with retries under an idempotency key) happens in the\n * background. Call {@link UsageMeter.flush} before a serverless handler returns so\n * pending records aren't lost when the process freezes.\n */\nexport interface UsageMeter {\n /** Enqueue a usage event. Returns immediately, never throws. */\n record(record: UsageRecord): void;\n /**\n * Enqueue a usage event whose record resolves LATER (e.g. a streamed response\n * whose token totals arrive as a promise). Registers the pending work immediately\n * so {@link UsageMeter.flush} awaits it — a `null` resolution or a rejected source\n * is dropped. Returns immediately, never throws.\n */\n recordAsync(source: Promise<UsageRecord | null>): void;\n /** Resolve once every buffered dispatch has settled (delivered or dropped). */\n flush(): Promise<void>;\n /** Records still in flight (queued or retrying). */\n readonly pending: number;\n}\n\nconst DEFAULT_MAX_ATTEMPTS = 5;\nconst DEFAULT_RETRY_BASE_MS = 250;\n\nfunction newIdempotencyKey(): string {\n return crypto.randomUUID();\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Build a {@link UsageMeter} bound to a billow client (or any `{ track }`). */\nexport function createUsageMeter(client: TrackClient, options: MeterOptions = {}): UsageMeter {\n const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);\n const retryBaseMs = Math.max(0, options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS);\n const inFlight = new Set<Promise<void>>();\n\n async function dispatch(record: UsageRecord): Promise<void> {\n // Freeze one idempotency key for the record so every retry is the SAME call.\n const input: UsageRecord = {\n ...record,\n idempotencyKey: record.idempotencyKey ?? newIdempotencyKey(),\n };\n let lastError: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n await client.track(input);\n return;\n } catch (error) {\n lastError = error;\n if (attempt < maxAttempts) await sleep(retryBaseMs * 2 ** (attempt - 1));\n }\n }\n try {\n options.onError?.(lastError, input);\n } catch {\n // A throwing onError must not reject the tracked dispatch (which would make\n // flush() reject) — the meter's contract is that it never throws.\n }\n }\n\n /** Register a unit of metering work so `flush()`/`pending` account for it. */\n function track(work: Promise<void>): void {\n const p = work.finally(() => {\n inFlight.delete(p);\n });\n inFlight.add(p);\n }\n\n return {\n record(record) {\n track(dispatch(record));\n },\n recordAsync(source) {\n // Chain resolve-then-dispatch into ONE tracked unit so flush() covers work\n // whose record isn't known yet. A null record or a rejected source is dropped.\n track(\n source.then(\n (record) => (record ? dispatch(record) : undefined),\n () => undefined,\n ),\n );\n },\n flush() {\n return Promise.all([...inFlight]).then(() => undefined);\n },\n get pending() {\n return inFlight.size;\n },\n };\n}\n\n/**\n * Derive a usage record from a completed call. Return `null` to skip metering it\n * (e.g. a cache hit with no billable usage). Receives the resolved result and the\n * original call arguments.\n */\nexport type UsageExtractor<A extends unknown[], R> = (result: R, args: A) => UsageRecord | null;\n\n/**\n * Wrap any async function so each successful call auto-records usage via `meter` —\n * the generic ingestion primitive. Point it at any source by supplying an `extract`.\n * Metering runs AFTER the call resolves and never blocks it; an extractor that\n * throws is swallowed. If the wrapped call itself rejects, nothing is recorded and\n * the error propagates unchanged.\n */\nexport function meterFunction<A extends unknown[], R>(\n fn: (...args: A) => Promise<R>,\n meter: UsageMeter,\n extract: UsageExtractor<A, R>,\n): (...args: A) => Promise<R> {\n return async (...args: A) => {\n const result = await fn(...args);\n try {\n const record = extract(result, args);\n if (record) meter.record(record);\n } catch {\n // Metering must never break the wrapped call — drop extractor errors.\n }\n return result;\n };\n}\n\n/**\n * Yield every value of `source` straight through to the consumer while calling\n * `onValue` for each and `onDone` when iteration finishes (including an early\n * `break`/`return` — it runs in a `finally`). Taps a stream WITHOUT consuming it:\n * the caller still receives every chunk. Tap callbacks that throw are swallowed so\n * they can't break the stream.\n */\nexport async function* tapAsyncIterable<T>(\n source: AsyncIterable<T>,\n taps: { onValue?: (value: T) => void; onDone?: () => void },\n): AsyncGenerator<T> {\n try {\n for await (const value of source) {\n try {\n taps.onValue?.(value);\n } catch {\n // never let a tap break the passthrough\n }\n yield value;\n }\n } finally {\n try {\n taps.onDone?.();\n } catch {\n // never let a tap break the passthrough\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// LLM token metering\n// ---------------------------------------------------------------------------\n\n/** Normalized token counts, whatever the provider's field naming. */\nexport interface TokenCounts {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n}\n\n/** Which token count a meter records as its `value` (default `\"total\"`). */\nexport type TokenCount = \"total\" | \"prompt\" | \"completion\";\n\nfunction firstNumber(...values: unknown[]): number | null {\n for (const v of values) if (typeof v === \"number\" && Number.isFinite(v)) return v;\n return null;\n}\n\n/**\n * Read token counts from a provider usage object, accepting OpenAI (`prompt_tokens`\n * / `completion_tokens` / `total_tokens`), Vercel AI SDK v4 (`promptTokens` / …) and\n * v5 (`inputTokens` / `outputTokens`) shapes. Returns `null` when no token fields\n * are present. `totalTokens` is derived from prompt+completion when not supplied.\n */\nexport function normalizeTokenUsage(usage: unknown): TokenCounts | null {\n if (typeof usage !== \"object\" || usage === null) return null;\n const u = usage as {\n prompt_tokens?: unknown;\n promptTokens?: unknown;\n inputTokens?: unknown;\n completion_tokens?: unknown;\n completionTokens?: unknown;\n outputTokens?: unknown;\n total_tokens?: unknown;\n totalTokens?: unknown;\n };\n const prompt = firstNumber(u.prompt_tokens, u.promptTokens, u.inputTokens);\n const completion = firstNumber(u.completion_tokens, u.completionTokens, u.outputTokens);\n const total = firstNumber(u.total_tokens, u.totalTokens);\n if (prompt === null && completion === null && total === null) return null;\n const promptTokens = prompt ?? 0;\n const completionTokens = completion ?? 0;\n return {\n promptTokens,\n completionTokens,\n totalTokens: total ?? promptTokens + completionTokens,\n };\n}\n\nfunction pickCount(counts: TokenCounts, which: TokenCount): number {\n if (which === \"prompt\") return counts.promptTokens;\n if (which === \"completion\") return counts.completionTokens;\n return counts.totalTokens;\n}\n\n/** A value supplied directly, or resolved from a call's params. */\ntype Resolvable<T, P> = T | ((params: P) => T);\n\nfunction resolveValue<T, P>(r: Resolvable<T, P>, params: P): T {\n return typeof r === \"function\" ? (r as (p: P) => T)(params) : r;\n}\n\nexport interface MeterLLMOptions<P = unknown> {\n /** The buffered meter that dispatches to `track()`. */\n meter: UsageMeter;\n /** The metered feature (token meter) id — static, or resolved per call. */\n feature: Resolvable<string, P>;\n /**\n * The billow customer to bill. Static for single-tenant, or resolved per call\n * (e.g. from the OpenAI `user` param) for multi-tenant. Returning `null`/`undefined`\n * skips metering that call — there's no customer to attribute it to.\n */\n customer: Resolvable<string | null | undefined, P>;\n /** Which token count to record as the meter value (default `\"total\"`). */\n count?: TokenCount;\n /** Extra event properties merged onto each record (`model` is added automatically). */\n properties?: (params: P, tokens: TokenCounts) => Record<string, string | number>;\n}\n\n/**\n * Build the usage record for one LLM call from its usage + model, or `null` to skip.\n * TOTAL by contract — a throwing `customer`/`feature`/`properties` resolver (e.g. an\n * unexpected params shape) drops the record rather than surfacing in the wrapped\n * call, so every caller (sync or async) inherits the non-blocking guarantee here.\n */\nfunction buildTokenRecord<P>(\n options: MeterLLMOptions<P>,\n params: P,\n usage: unknown,\n model: unknown,\n): UsageRecord | null {\n const counts = normalizeTokenUsage(usage);\n if (!counts) return null;\n try {\n const customerId = resolveValue(options.customer, params);\n if (!customerId) return null;\n const properties: Record<string, string | number> = {\n tokens_prompt: counts.promptTokens,\n tokens_completion: counts.completionTokens,\n tokens_total: counts.totalTokens,\n ...(typeof model === \"string\" ? { model } : {}),\n ...options.properties?.(params, counts),\n };\n return {\n customerId,\n featureId: resolveValue(options.feature, params),\n value: pickCount(counts, options.count ?? \"total\"),\n properties,\n };\n } catch {\n return null; // a resolver mistake must never break the wrapped call\n }\n}\n\nfunction isPromiseLike(v: unknown): v is Promise<unknown> {\n return (\n (typeof v === \"object\" || typeof v === \"function\") &&\n v !== null &&\n typeof (v as { then?: unknown }).then === \"function\"\n );\n}\n\n/** The two params slots an OpenAI-style `create(body, requestOptions?)` takes. */\ntype CreateArgs = [params: Record<string, unknown>, ...rest: unknown[]];\n\nfunction wrapOpenAICreate(\n create: (...args: unknown[]) => unknown,\n thisArg: unknown,\n options: MeterLLMOptions<Record<string, unknown>>,\n): (...args: CreateArgs) => unknown {\n return (params, ...rest) => {\n const returned = Reflect.apply(create, thisArg, [params, ...rest]);\n // openai-node returns a thenable (APIPromise) for BOTH streaming and not.\n if (!isPromiseLike(returned)) return returned;\n\n if ((params as { stream?: unknown }).stream === true) {\n // Streaming: TAP iteration to read the usage the final chunk carries (present\n // when the caller sets `stream_options.include_usage`) WITHOUT dropping the\n // OpenAI return contract — the APIPromise keeps its helpers and the resolved\n // Stream keeps its methods; we only override `Symbol.asyncIterator`.\n return proxyResolved(returned, (stream) =>\n typeof stream === \"object\" && stream !== null\n ? proxyAsyncIterable(stream, () =>\n tapOpenAIStream(stream as AsyncIterable<unknown>, params, options),\n )\n : stream,\n );\n }\n // Non-streaming: record from the resolved completion's `.usage`, tracked by the\n // meter so `flush()` awaits it. The caller receives the ORIGINAL promise\n // untouched (APIPromise helpers intact); a failed call resolves to no record.\n options.meter.recordAsync(returned.then((resp) => completionRecord(resp, params, options)));\n return returned;\n };\n}\n\nfunction completionRecord(\n resp: unknown,\n params: Record<string, unknown>,\n options: MeterLLMOptions<Record<string, unknown>>,\n): UsageRecord | null {\n if (typeof resp !== \"object\" || resp === null) return null;\n const r = resp as { usage?: unknown; model?: unknown };\n return buildTokenRecord(options, params, r.usage, r.model);\n}\n\n/**\n * Proxy a thenable so awaiting it yields `map(resolvedValue)`, while every other\n * property/method (an APIPromise's `.withResponse()`/`.asResponse()`, …) passes\n * straight through. Lets us tap the resolved value without dropping the return\n * contract.\n */\nfunction proxyResolved(\n source: Promise<unknown>,\n map: (value: unknown) => unknown,\n): Promise<unknown> {\n const mappedThen = (onF?: (v: unknown) => unknown, onR?: (e: unknown) => unknown) =>\n source.then((v) => (onF ? onF(map(v)) : map(v)), onR);\n return new Proxy(source, {\n get(target, prop) {\n // Route `then`/`catch`/`finally` through the mapping so awaiting via ANY of\n // them yields the tapped value — binding the native methods to `target` would\n // resolve the raw (un-tapped) stream and silently skip metering.\n if (prop === \"then\") return mappedThen;\n if (prop === \"catch\") return (onR: (e: unknown) => unknown) => mappedThen(undefined, onR);\n if (prop === \"finally\") {\n return (cb?: () => void) =>\n mappedThen(\n (v) => {\n cb?.();\n return v;\n },\n (e) => {\n cb?.();\n throw e;\n },\n );\n }\n const value = Reflect.get(target, prop);\n return typeof value === \"function\" ? value.bind(target) : value;\n },\n }) as Promise<unknown>;\n}\n\n/**\n * Proxy an async-iterable object so `for await` runs `makeIterator()` (our tap),\n * while every other property/method (a Stream's `.tee()`, `.toReadableStream()`, …)\n * passes straight through. Taps iteration WITHOUT replacing the object.\n */\nfunction proxyAsyncIterable(source: object, makeIterator: () => AsyncIterator<unknown>): object {\n return new Proxy(source, {\n get(target, prop) {\n if (prop === Symbol.asyncIterator) return makeIterator;\n const value = Reflect.get(target, prop);\n return typeof value === \"function\" ? value.bind(target) : value;\n },\n });\n}\n\n/** Usage + model folded out of a streamed response, ready for {@link buildTokenRecord}. */\ninterface StreamUsage {\n usage?: unknown;\n model?: unknown;\n}\n\n/**\n * Meter a streamed response: tap the stream (yielding every chunk untouched), fold\n * each chunk into an accumulator via `reduce`, and record ONE usage event when\n * iteration finishes. This is the reusable middle between the generic\n * {@link tapAsyncIterable} and a specific provider's chunk shape — a new streaming\n * source (Anthropic SSE, etc.) plugs in by supplying only its `reduce`.\n */\nfunction meterStream<C, P>(\n stream: AsyncIterable<C>,\n params: P,\n options: MeterLLMOptions<P>,\n reduce: (chunk: C, acc: StreamUsage) => void,\n): AsyncGenerator<C> {\n const acc: StreamUsage = {};\n return tapAsyncIterable(stream, {\n onValue: (chunk) => reduce(chunk, acc),\n onDone: () => {\n const record = buildTokenRecord(options, params, acc.usage, acc.model);\n if (record) options.meter.record(record);\n },\n });\n}\n\nfunction tapOpenAIStream(\n stream: AsyncIterable<unknown>,\n params: Record<string, unknown>,\n options: MeterLLMOptions<Record<string, unknown>>,\n): AsyncGenerator<unknown> {\n return meterStream(stream, params, options, (chunk, acc) => {\n if (typeof chunk !== \"object\" || chunk === null) return;\n const c = chunk as { usage?: unknown; model?: unknown };\n if (c.usage) acc.usage = c.usage; // the final chunk carries usage (include_usage)\n if (c.model && acc.model === undefined) acc.model = c.model;\n });\n}\n\n/**\n * Wrap an OpenAI-compatible client so every `chat.completions.create` call\n * auto-meters its token usage. Returns a proxy of the SAME type — use it exactly as\n * before; every other property/method passes straight through untouched.\n *\n * Reads the token count from the provider's own response (no re-tokenizing), for\n * both blocking and streaming calls. For a STREAMED call, pass\n * `stream_options: { include_usage: true }` so the final chunk carries usage —\n * otherwise a streamed call can't be metered (billow never mutates your request).\n *\n * A streamed response is metered when consumed the standard way: `await` the call,\n * then `for await` the stream. The `Stream` helpers (`.tee()`, `.toReadableStream()`)\n * and the un-awaited `APIPromise` helpers (`.withResponse()`, `.asResponse()`) are\n * PRESERVED (they keep working), but consuming through them bypasses the iteration\n * tap, so those calls are not token-metered.\n *\n * Works with any client exposing `chat.completions.create` returning a `.usage`\n * shape: OpenAI, Azure OpenAI, Together, Groq, OpenRouter, and other compatibles.\n *\n * const openai = meterOpenAI(new OpenAI(), { meter, feature: \"tokens\", customer });\n * const res = await openai.chat.completions.create({ model, messages }); // metered\n */\nexport function meterOpenAI<T extends object>(\n client: T,\n options: MeterLLMOptions<Record<string, unknown>>,\n): T {\n const root = client as { chat?: { completions?: { create?: unknown } } };\n const completions = root.chat?.completions;\n const create = completions?.create;\n if (typeof create !== \"function\" || !completions) {\n throw new TypeError(\"meterOpenAI: expected client.chat.completions.create to be a function\");\n }\n const wrapped = wrapOpenAICreate(create as (...args: unknown[]) => unknown, completions, options);\n return replaceCreate(client, wrapped);\n}\n\n/**\n * Return a non-mutating proxy of `client` that swaps ONLY `chat.completions.create`\n * for `wrapped`, passing every other access straight through. Untouched methods keep\n * their real receiver, so `this` stays correct.\n */\nfunction replaceCreate<T extends object>(client: T, wrapped: (...args: CreateArgs) => unknown): T {\n const root = client as { chat: { completions: object } };\n const completionsProxy = new Proxy(root.chat.completions, {\n get: (t, p, r) => (p === \"create\" ? wrapped : Reflect.get(t, p, r)),\n });\n const chatProxy = new Proxy(root.chat, {\n get: (t, p, r) => (p === \"completions\" ? completionsProxy : Reflect.get(t, p, r)),\n });\n return new Proxy(client, {\n get: (t, p, r) => (p === \"chat\" ? chatProxy : Reflect.get(t, p, r)),\n });\n}\n\n/**\n * Meter a Vercel AI SDK result (the object `generateText`/`streamText` resolves to).\n * Reads `result.usage` — an object (`generateText`) or a Promise (`streamText`) — and\n * records the token count, non-blocking. Never touches `result.textStream`, so a\n * streamed response is delivered to your consumer untouched. Accepts AI SDK v4\n * (`promptTokens`/`completionTokens`) and v5 (`inputTokens`/`outputTokens`) shapes.\n *\n * const result = streamText({ model, prompt });\n * meterAIResult(result, { meter, feature: \"tokens\", customer: customerId });\n * return result.toDataStreamResponse();\n */\nexport function meterAIResult(\n result: { usage?: unknown; response?: unknown },\n options: MeterLLMOptions<void>,\n): void {\n const model = extractAIModel(result.response);\n const usage = result.usage;\n if (isPromiseLike(usage)) {\n // `streamText` resolves usage LATE — hand the meter the whole resolve→record\n // chain so `flush()` (the README's serverless guidance) awaits it instead of\n // snapshotting an empty in-flight set and dropping the usage.\n options.meter.recordAsync(usage.then((u) => buildTokenRecord(options, undefined, u, model)));\n return;\n }\n const record = buildTokenRecord(options, undefined, usage, model);\n if (record) options.meter.record(record);\n}\n\n/** Best-effort model id from an AI SDK result's `response` (skipped if it's a promise). */\nfunction extractAIModel(response: unknown): unknown {\n if (typeof response !== \"object\" || response === null) return undefined;\n const m = (response as { modelId?: unknown }).modelId;\n return typeof m === \"string\" ? m : undefined;\n}\n"]}