@usagetap/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1559 @@
1
+ // src/errors.ts
2
+ var UsageTapError = class extends Error {
3
+ code;
4
+ status;
5
+ retryable;
6
+ correlationId;
7
+ details;
8
+ constructor(code, message, init = {}) {
9
+ super(message, init.cause ? { cause: init.cause } : void 0);
10
+ this.name = "UsageTapError";
11
+ this.code = code;
12
+ this.status = init.status;
13
+ this.retryable = init.retryable ?? false;
14
+ this.correlationId = init.correlationId;
15
+ this.details = init.details;
16
+ }
17
+ toJSON() {
18
+ return {
19
+ name: this.name,
20
+ message: this.message,
21
+ code: this.code,
22
+ status: this.status,
23
+ retryable: this.retryable,
24
+ correlationId: this.correlationId,
25
+ details: this.details
26
+ };
27
+ }
28
+ };
29
+ function isUsageTapError(error) {
30
+ return error instanceof UsageTapError;
31
+ }
32
+
33
+ // src/idempotency.ts
34
+ function createIdempotencyKey() {
35
+ if (typeof globalThis.crypto?.randomUUID === "function") {
36
+ return globalThis.crypto.randomUUID();
37
+ }
38
+ const random = () => Math.random().toString(16).slice(2, 10);
39
+ return `${random()}-${random()}`;
40
+ }
41
+
42
+ // src/retry.ts
43
+ var DEFAULTS = {
44
+ maxAttempts: 3,
45
+ baseDelayMs: 250,
46
+ maxDelayMs: 5e3,
47
+ jitterRatio: 0.2
48
+ };
49
+ function resolveRetryOptions(base, override) {
50
+ const merged = { ...DEFAULTS, ...base, ...override };
51
+ return {
52
+ maxAttempts: Math.max(1, Math.floor(merged.maxAttempts)),
53
+ baseDelayMs: Math.max(0, merged.baseDelayMs),
54
+ maxDelayMs: Math.max(merged.baseDelayMs, merged.maxDelayMs),
55
+ jitterRatio: Math.min(Math.max(merged.jitterRatio, 0), 1)
56
+ };
57
+ }
58
+ async function sleep(delayMs, signal) {
59
+ if (delayMs <= 0) {
60
+ signal?.throwIfAborted?.();
61
+ return;
62
+ }
63
+ await new Promise((resolve, reject) => {
64
+ const timer = setTimeout(() => {
65
+ cleanup();
66
+ resolve();
67
+ }, delayMs);
68
+ const cleanup = () => {
69
+ clearTimeout(timer);
70
+ signal?.removeEventListener("abort", onAbort);
71
+ };
72
+ const onAbort = () => {
73
+ cleanup();
74
+ const abortError = new Error("Aborted");
75
+ abortError.name = "AbortError";
76
+ reject(abortError);
77
+ };
78
+ if (signal) {
79
+ if (signal.aborted) {
80
+ onAbort();
81
+ return;
82
+ }
83
+ signal.addEventListener("abort", onAbort, { once: true });
84
+ }
85
+ });
86
+ }
87
+ function computeDelay(attempt, options) {
88
+ const exp = options.baseDelayMs * Math.pow(2, attempt - 1);
89
+ const capped = Math.min(exp, options.maxDelayMs);
90
+ const jitter = capped * options.jitterRatio;
91
+ const min = capped - jitter;
92
+ const max = capped + jitter;
93
+ return Math.max(0, Math.random() * (max - min) + min);
94
+ }
95
+ async function runWithRetry(operation, options, shouldRetry, onSchedule, signal) {
96
+ let attempt = 0;
97
+ let lastError;
98
+ while (attempt < options.maxAttempts) {
99
+ attempt += 1;
100
+ signal?.throwIfAborted?.();
101
+ try {
102
+ return await operation(attempt);
103
+ } catch (error) {
104
+ lastError = error;
105
+ if (attempt >= options.maxAttempts || !shouldRetry(error)) {
106
+ throw error;
107
+ }
108
+ const delayMs = computeDelay(attempt, options);
109
+ onSchedule?.(attempt, delayMs, error);
110
+ await sleep(delayMs, signal);
111
+ }
112
+ }
113
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
114
+ }
115
+
116
+ // src/client.ts
117
+ var CALL_BEGIN_PATH = "call_begin";
118
+ var CALL_END_PATH = "call_end";
119
+ var AUTH_HEADER = "authorization";
120
+ var API_KEY_HEADER = "x-api-key";
121
+ var CORRELATION_HEADER = "x-usage-correlation-id";
122
+ var IDEMPOTENCY_HEADER = "idempotency-key";
123
+ var SDK_HEADER = "x-usage-sdk";
124
+ var USER_AGENT = "UsageTapClient";
125
+ var CANONICAL_MEDIA_TYPE = "application/vnd.usagetap.v1+json";
126
+ var SDK_VERSION = "0.1.0" ;
127
+ var HAS_WINDOW = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
128
+ var UsageTapClient = class {
129
+ apiKey;
130
+ baseUrl;
131
+ fetchImpl;
132
+ defaultFeature;
133
+ defaultTags;
134
+ defaultHeaders;
135
+ retryDefaults;
136
+ idempotencyGenerator;
137
+ logFn;
138
+ authHeader;
139
+ autoIdempotency;
140
+ constructor(options) {
141
+ if (!options) {
142
+ throw new UsageTapError(
143
+ "USAGETAP_BAD_REQUEST",
144
+ "UsageTapClient options are required"
145
+ );
146
+ }
147
+ if (!options.apiKey) {
148
+ throw new UsageTapError(
149
+ "USAGETAP_BAD_REQUEST",
150
+ "UsageTapClient requires an apiKey"
151
+ );
152
+ }
153
+ if (!options.baseUrl) {
154
+ throw new UsageTapError(
155
+ "USAGETAP_BAD_REQUEST",
156
+ "UsageTapClient requires a baseUrl"
157
+ );
158
+ }
159
+ if (HAS_WINDOW && !options.allowBrowser) {
160
+ throw new UsageTapError(
161
+ "USAGETAP_BROWSER_RUNTIME",
162
+ "UsageTapClient is designed for server-side environments. Pass allowBrowser=true only for testing."
163
+ );
164
+ }
165
+ const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
166
+ if (typeof fetchCandidate !== "function") {
167
+ throw new UsageTapError(
168
+ "USAGETAP_NETWORK_ERROR",
169
+ "A global fetch implementation was not found. Pass fetchImpl in UsageTapClientOptions."
170
+ );
171
+ }
172
+ const normalizedBaseUrl = normalizeBaseUrl(options.baseUrl);
173
+ this.baseUrl = new URL(normalizedBaseUrl);
174
+ this.apiKey = options.apiKey;
175
+ this.fetchImpl = fetchCandidate;
176
+ this.defaultFeature = options.defaultFeature;
177
+ this.defaultTags = options.defaultTags?.length ? dedupeStrings(options.defaultTags) : void 0;
178
+ this.defaultHeaders = options.headers ? normalizeHeaderDictionary(options.headers) : {};
179
+ this.retryDefaults = resolveRetryOptions(options.retries);
180
+ this.idempotencyGenerator = options.idempotencyGenerator ?? createIdempotencyKey;
181
+ this.logFn = options.onLog;
182
+ this.authHeader = options.useApiKeyHeader ? API_KEY_HEADER : AUTH_HEADER;
183
+ this.autoIdempotency = options.autoIdempotency ?? true;
184
+ }
185
+ async beginCall(request, options = {}) {
186
+ const idempotencyKey = request.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
187
+ const payload = {
188
+ ...request,
189
+ feature: request.feature ?? this.defaultFeature,
190
+ tags: this.mergeTags(request.tags)
191
+ };
192
+ if (idempotencyKey) {
193
+ payload.idempotency = idempotencyKey;
194
+ }
195
+ const response = await this.request(
196
+ CALL_BEGIN_PATH,
197
+ payload,
198
+ {
199
+ ...options,
200
+ idempotencyKey
201
+ }
202
+ );
203
+ return response;
204
+ }
205
+ async endCall(request, options = {}) {
206
+ if (!request?.callId) {
207
+ throw new UsageTapError(
208
+ "USAGETAP_BAD_REQUEST",
209
+ "endCall requires callId"
210
+ );
211
+ }
212
+ const payload = { ...request };
213
+ const response = await this.request(
214
+ CALL_END_PATH,
215
+ payload,
216
+ options
217
+ );
218
+ return response;
219
+ }
220
+ async withUsage(beginRequest, handler, options = {}) {
221
+ const idempotencyKey = beginRequest.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
222
+ const beginPayload = idempotencyKey ? { ...beginRequest, idempotency: idempotencyKey } : { ...beginRequest };
223
+ const beginResponse = await this.beginCall(beginPayload, options);
224
+ let usage = {};
225
+ const initialStripeCustomerId = typeof beginResponse.data.stripeCustomerId === "string" ? beginResponse.data.stripeCustomerId : typeof beginRequest.stripeCustomerId === "string" ? beginRequest.stripeCustomerId : void 0;
226
+ if (initialStripeCustomerId) {
227
+ usage = { ...usage, stripeCustomerId: initialStripeCustomerId };
228
+ }
229
+ let errorPayload;
230
+ let handlerResult;
231
+ let handlerError;
232
+ let endCallError;
233
+ const context = {
234
+ begin: beginResponse,
235
+ setUsage: (u) => {
236
+ usage = { ...usage, ...u };
237
+ },
238
+ setError: (err) => {
239
+ errorPayload = err;
240
+ }
241
+ };
242
+ try {
243
+ handlerResult = await handler(context);
244
+ } catch (error) {
245
+ handlerError = error;
246
+ if (!errorPayload) {
247
+ errorPayload = {
248
+ code: options.defaultErrorCode ?? "VENDOR_ERROR",
249
+ message: error instanceof Error ? error.message : String(error)
250
+ };
251
+ }
252
+ } finally {
253
+ try {
254
+ await this.endCall(
255
+ {
256
+ callId: beginResponse.data.callId,
257
+ ...usage,
258
+ error: errorPayload
259
+ },
260
+ {
261
+ ...options,
262
+ correlationId: beginResponse.correlationId
263
+ }
264
+ );
265
+ } catch (error) {
266
+ endCallError = error;
267
+ }
268
+ }
269
+ if (handlerError) {
270
+ throw handlerError;
271
+ }
272
+ if (endCallError) {
273
+ throw wrapEndCallError(endCallError, beginResponse.correlationId);
274
+ }
275
+ return handlerResult;
276
+ }
277
+ async request(path, payload, options) {
278
+ const url = new URL(path, this.baseUrl).toString();
279
+ const body = payload !== void 0 ? JSON.stringify(payload) : void 0;
280
+ const headers = this.composeHeaders(body, options);
281
+ const resolvedRetry = resolveRetryOptions(
282
+ this.retryDefaults,
283
+ options.retries
284
+ );
285
+ const startTime = () => typeof performance !== "undefined" ? performance.now() : Date.now();
286
+ return runWithRetry(
287
+ async (attempt) => {
288
+ const startedAt = startTime();
289
+ this.log({
290
+ event: "request:start",
291
+ path,
292
+ attempt,
293
+ idempotencyKey: options.idempotencyKey,
294
+ correlationId: options.correlationId
295
+ });
296
+ const response = await this.performFetch({
297
+ url,
298
+ method: "POST",
299
+ headers,
300
+ body,
301
+ signal: options.signal
302
+ });
303
+ this.log({
304
+ event: "request:success",
305
+ path,
306
+ attempt,
307
+ idempotencyKey: options.idempotencyKey,
308
+ correlationId: response.correlationId,
309
+ elapsedMs: startTime() - startedAt
310
+ });
311
+ return response;
312
+ },
313
+ resolvedRetry,
314
+ (error) => this.shouldRetry(error),
315
+ (attempt, delayMs, error) => {
316
+ this.log({
317
+ event: "retry:scheduled",
318
+ path,
319
+ attempt,
320
+ idempotencyKey: options.idempotencyKey,
321
+ correlationId: options.correlationId,
322
+ error,
323
+ elapsedMs: delayMs
324
+ });
325
+ },
326
+ options.signal
327
+ ).catch((error) => {
328
+ this.log({
329
+ event: "retry:exhausted",
330
+ path,
331
+ attempt: resolvedRetry.maxAttempts,
332
+ idempotencyKey: options.idempotencyKey,
333
+ correlationId: options.correlationId,
334
+ error
335
+ });
336
+ throw error;
337
+ });
338
+ }
339
+ async performFetch(init) {
340
+ let response;
341
+ try {
342
+ response = await this.fetchImpl(init.url, {
343
+ method: init.method,
344
+ headers: init.headers,
345
+ body: init.body,
346
+ signal: init.signal
347
+ });
348
+ } catch (error) {
349
+ throw new UsageTapError(
350
+ "USAGETAP_NETWORK_ERROR",
351
+ "Failed to reach UsageTap",
352
+ {
353
+ retryable: true,
354
+ cause: error
355
+ }
356
+ );
357
+ }
358
+ const correlationId = response.headers.get(CORRELATION_HEADER) ?? void 0;
359
+ const text = await response.text();
360
+ let payload;
361
+ if (text) {
362
+ try {
363
+ payload = JSON.parse(text);
364
+ } catch (error) {
365
+ throw new UsageTapError(
366
+ "USAGETAP_INVALID_RESPONSE",
367
+ "UsageTap returned invalid JSON",
368
+ {
369
+ retryable: false,
370
+ correlationId,
371
+ cause: error
372
+ }
373
+ );
374
+ }
375
+ }
376
+ if (!response.ok) {
377
+ throw this.toHttpError(response.status, payload, correlationId);
378
+ }
379
+ if (!payload?.result || payload.result.status !== "ACCEPTED") {
380
+ throw this.toApiError(payload, correlationId);
381
+ }
382
+ const resolvedCorrelation = payload.correlationId ?? correlationId;
383
+ if (payload.data === void 0 || payload.data === null || !resolvedCorrelation) {
384
+ throw new UsageTapError(
385
+ "USAGETAP_INVALID_RESPONSE",
386
+ "UsageTap response missing data or correlationId",
387
+ {
388
+ correlationId: resolvedCorrelation ?? correlationId
389
+ }
390
+ );
391
+ }
392
+ return {
393
+ result: {
394
+ status: payload.result.status,
395
+ code: payload.result.code,
396
+ message: payload.result.message,
397
+ timestamp: payload.result.timestamp
398
+ },
399
+ data: payload.data,
400
+ correlationId: resolvedCorrelation
401
+ };
402
+ }
403
+ composeHeaders(body, options) {
404
+ const headers = {
405
+ ...this.defaultHeaders,
406
+ [SDK_HEADER]: `js/${SDK_VERSION}`,
407
+ "user-agent": `${USER_AGENT}/${SDK_VERSION}`,
408
+ "content-type": "application/json",
409
+ accept: CANONICAL_MEDIA_TYPE
410
+ };
411
+ if (this.authHeader === API_KEY_HEADER) {
412
+ headers[API_KEY_HEADER] = this.apiKey;
413
+ } else {
414
+ headers[AUTH_HEADER] = `Bearer ${this.apiKey}`;
415
+ }
416
+ if (options.idempotencyKey) {
417
+ headers[IDEMPOTENCY_HEADER] = options.idempotencyKey;
418
+ }
419
+ if (options.correlationId) {
420
+ headers[CORRELATION_HEADER] = options.correlationId;
421
+ }
422
+ if (!body) {
423
+ delete headers["content-type"];
424
+ }
425
+ if (options.headers) {
426
+ Object.assign(headers, normalizeHeaderDictionary(options.headers));
427
+ }
428
+ return headers;
429
+ }
430
+ log(entry) {
431
+ this.logFn?.(entry);
432
+ }
433
+ mergeTags(tags) {
434
+ if (!tags && !this.defaultTags) {
435
+ return void 0;
436
+ }
437
+ const combined = [...this.defaultTags ?? [], ...tags ?? []].filter(
438
+ Boolean
439
+ );
440
+ return combined.length ? dedupeStrings(combined) : void 0;
441
+ }
442
+ shouldRetry(error) {
443
+ if (isUsageTapError(error)) {
444
+ return Boolean(error.retryable);
445
+ }
446
+ if (error instanceof Error && error.name === "AbortError") {
447
+ return false;
448
+ }
449
+ return false;
450
+ }
451
+ toHttpError(status, payload, correlationId) {
452
+ const code = mapStatusToErrorCode(status);
453
+ const retryable = isRetryableStatus(status);
454
+ const message = payload?.error?.message ?? payload?.result?.message ?? `UsageTap responded with HTTP ${status}`;
455
+ return new UsageTapError(code, message, {
456
+ status,
457
+ retryable,
458
+ correlationId: payload?.correlationId ?? correlationId,
459
+ details: sanitizeDetails(payload)
460
+ });
461
+ }
462
+ toApiError(payload, correlationId) {
463
+ const normalizedCode = payload?.error?.code ?? payload?.result?.code ?? "UNKNOWN";
464
+ const retryable = isRetryableApiCode(normalizedCode);
465
+ const message = payload?.error?.message ?? payload?.result?.message ?? "UsageTap reported an error";
466
+ return new UsageTapError(mapApiCodeToError(normalizedCode), message, {
467
+ retryable,
468
+ correlationId: payload?.correlationId ?? correlationId,
469
+ details: sanitizeDetails(payload)
470
+ });
471
+ }
472
+ };
473
+ function mapStatusToErrorCode(status) {
474
+ if (status === 401 || status === 403) return "USAGETAP_AUTH_ERROR";
475
+ if (status === 400 || status === 404 || status === 409)
476
+ return "USAGETAP_BAD_REQUEST";
477
+ if (status === 429) return "USAGETAP_RATE_LIMITED";
478
+ if (status >= 500) return "USAGETAP_SERVER_ERROR";
479
+ return "USAGETAP_INVALID_RESPONSE";
480
+ }
481
+ function isRetryableStatus(status) {
482
+ return status === 408 || status === 425 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
483
+ }
484
+ function isRetryableApiCode(code) {
485
+ const normalized = code.toUpperCase();
486
+ return normalized.includes("TRANSIENT") || normalized.includes("RETRY") || normalized.includes("TIMEOUT") || normalized.includes("THROTTLE") || normalized.includes("RATE_LIMIT");
487
+ }
488
+ function mapApiCodeToError(code) {
489
+ const normalized = code.toUpperCase();
490
+ if (normalized.includes("AUTH") || normalized.includes("TOKEN")) {
491
+ return "USAGETAP_AUTH_ERROR";
492
+ }
493
+ if (normalized.includes("RATE") || normalized.includes("THROTTLE")) {
494
+ return "USAGETAP_RATE_LIMITED";
495
+ }
496
+ if (normalized.includes("SERVER") || normalized.includes("TRANSIENT")) {
497
+ return "USAGETAP_SERVER_ERROR";
498
+ }
499
+ if (normalized.includes("IDEMPOTENCY") || normalized.includes("VALIDATION") || normalized.includes("REQUEST")) {
500
+ return "USAGETAP_BAD_REQUEST";
501
+ }
502
+ return "USAGETAP_INVALID_RESPONSE";
503
+ }
504
+ function sanitizeDetails(payload) {
505
+ if (!payload) return void 0;
506
+ const details = {};
507
+ if (payload.result) details.result = payload.result;
508
+ if (payload.error) details.error = payload.error;
509
+ return Object.keys(details).length ? details : void 0;
510
+ }
511
+ function normalizeBaseUrl(baseUrl) {
512
+ const trimmed = baseUrl.trim();
513
+ if (!trimmed) return trimmed;
514
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
515
+ }
516
+ function normalizeHeaderDictionary(dict) {
517
+ return Object.keys(dict).reduce((acc, key) => {
518
+ acc[key.toLowerCase()] = dict[key];
519
+ return acc;
520
+ }, {});
521
+ }
522
+ function dedupeStrings(values) {
523
+ return Array.from(
524
+ new Set(values.map((value) => value.trim()).filter(Boolean))
525
+ );
526
+ }
527
+ function wrapEndCallError(error, correlationId) {
528
+ if (isUsageTapError(error)) {
529
+ return new UsageTapError("USAGETAP_END_CALL_ERROR", error.message, {
530
+ correlationId: error.correlationId ?? correlationId,
531
+ details: error.details,
532
+ cause: error
533
+ });
534
+ }
535
+ return new UsageTapError(
536
+ "USAGETAP_END_CALL_ERROR",
537
+ "Failed to finalize UsageTap call",
538
+ {
539
+ correlationId,
540
+ cause: error
541
+ }
542
+ );
543
+ }
544
+
545
+ // src/adapters/openai.ts
546
+ function createOpenAIAdapter(init) {
547
+ const { client, usageTap } = init;
548
+ return {
549
+ async invoke(params) {
550
+ const result = await usageTap.withUsage(
551
+ params.begin,
552
+ async (ctx) => {
553
+ const response = await params.call(client, {
554
+ hints: ctx.begin.data.vendorHints,
555
+ begin: ctx.begin
556
+ });
557
+ tryInferUsage(response, ctx.begin.data.vendorHints, params.extractUsage, ctx);
558
+ return {
559
+ data: response,
560
+ begin: ctx.begin
561
+ };
562
+ },
563
+ params.withUsageOptions
564
+ );
565
+ return result;
566
+ },
567
+ async invokeStream(params) {
568
+ const result = await usageTap.withUsage(
569
+ params.begin,
570
+ async (ctx) => {
571
+ const { stream, onComplete } = await params.call(client, {
572
+ hints: ctx.begin.data.vendorHints,
573
+ begin: ctx.begin
574
+ });
575
+ const wrapped = wrapStreamForUsageTap(stream, async () => {
576
+ if (!onComplete) return;
577
+ try {
578
+ const maybeUsage = await onComplete();
579
+ if (maybeUsage) {
580
+ ctx.setUsage(maybeUsage);
581
+ }
582
+ } catch (error) {
583
+ ctx.setError({
584
+ code: "USAGE_FINALIZE_ERROR",
585
+ message: error instanceof Error ? error.message : String(error)
586
+ });
587
+ throw error;
588
+ }
589
+ }, ctx);
590
+ const finalize = async () => {
591
+ await wrapped.__usageTapFinalize?.();
592
+ };
593
+ return {
594
+ stream: wrapped,
595
+ begin: ctx.begin,
596
+ finalize
597
+ };
598
+ },
599
+ params.withUsageOptions
600
+ );
601
+ return result;
602
+ }
603
+ };
604
+ }
605
+ function toNextResponse(stream, options = {}) {
606
+ const mode = options.mode ?? "text";
607
+ const headers = new Headers(options.headers ?? {});
608
+ if (mode === "sse") {
609
+ headers.set("content-type", "text/event-stream; charset=utf-8");
610
+ headers.set("cache-control", "no-cache, no-transform");
611
+ headers.set("connection", "keep-alive");
612
+ headers.set("x-accel-buffering", "no");
613
+ } else {
614
+ headers.set("content-type", options.contentType ?? "text/plain; charset=utf-8");
615
+ }
616
+ const encoder = new TextEncoder();
617
+ let iterator;
618
+ const body = new ReadableStream({
619
+ async start(controller) {
620
+ try {
621
+ const getIterator = stream[Symbol.asyncIterator];
622
+ if (typeof getIterator !== "function") {
623
+ controller.close();
624
+ return;
625
+ }
626
+ iterator = getIterator.call(stream);
627
+ while (true) {
628
+ const result = await iterator.next();
629
+ if (result.done) {
630
+ break;
631
+ }
632
+ const text = chunkToText(result.value);
633
+ if (!text) {
634
+ continue;
635
+ }
636
+ if (mode === "sse") {
637
+ controller.enqueue(encoder.encode(formatSsePayload(text, options.sse)));
638
+ } else {
639
+ controller.enqueue(encoder.encode(text));
640
+ }
641
+ }
642
+ controller.close();
643
+ } catch (error) {
644
+ controller.error(error);
645
+ } finally {
646
+ await stream.__usageTapFinalize?.();
647
+ }
648
+ },
649
+ async cancel() {
650
+ if (!iterator) {
651
+ const getIterator = stream[Symbol.asyncIterator];
652
+ if (typeof getIterator === "function") {
653
+ iterator = getIterator.call(stream);
654
+ }
655
+ }
656
+ if (iterator && typeof iterator.return === "function") {
657
+ await iterator.return();
658
+ }
659
+ await stream.__usageTapFinalize?.();
660
+ }
661
+ });
662
+ return new Response(body, { headers });
663
+ }
664
+ async function pipeToResponse(stream, res, options = {}) {
665
+ const mode = options.mode ?? "text";
666
+ if (mode === "sse") {
667
+ setHeaderIfPossible(res, "Content-Type", "text/event-stream; charset=utf-8");
668
+ setHeaderIfPossible(res, "Cache-Control", "no-cache, no-transform");
669
+ setHeaderIfPossible(res, "Connection", "keep-alive");
670
+ setHeaderIfPossible(res, "X-Accel-Buffering", "no");
671
+ } else {
672
+ setHeaderIfPossible(res, "Content-Type", options.contentType ?? "text/plain; charset=utf-8");
673
+ }
674
+ const encoder = new TextEncoder();
675
+ const iterator = stream[Symbol.asyncIterator]();
676
+ try {
677
+ while (true) {
678
+ const result = await iterator.next();
679
+ if (result.done) {
680
+ break;
681
+ }
682
+ const text = chunkToText(result.value);
683
+ if (!text) {
684
+ continue;
685
+ }
686
+ const payload = mode === "sse" ? formatSsePayload(text, options.sse) : text;
687
+ res.write(Buffer.from(encoder.encode(payload)));
688
+ res.flush?.();
689
+ }
690
+ } finally {
691
+ res.end();
692
+ await stream.__usageTapFinalize?.();
693
+ }
694
+ }
695
+ var USAGETAP_CORRELATION_HEADER = "x-usage-correlation-id";
696
+ function wrapOpenAI(client, usageTap, options = {}) {
697
+ if (!client) {
698
+ throw new UsageTapError("USAGETAP_BAD_REQUEST", "wrapOpenAI requires an OpenAI client instance");
699
+ }
700
+ const defaultContext = options.defaultContext;
701
+ const applyVendorHints = options.applyVendorHints !== false;
702
+ const proxiedChat = client.chat ? createChatProxy(client.chat, usageTap, defaultContext, applyVendorHints) : void 0;
703
+ const proxiedResponses = typeof client.responses !== "undefined" ? createResponsesProxy(client.responses, usageTap, defaultContext, applyVendorHints) : void 0;
704
+ const handler = {
705
+ get(target, prop, receiver) {
706
+ if (prop === "chat" && proxiedChat) {
707
+ return proxiedChat;
708
+ }
709
+ if (prop === "responses" && typeof target.responses !== "undefined") {
710
+ return proxiedResponses ?? Reflect.get(target, prop, receiver);
711
+ }
712
+ if (prop === "toNextResponse") {
713
+ return toNextResponse;
714
+ }
715
+ if (prop === "pipeToResponse") {
716
+ return pipeToResponse;
717
+ }
718
+ if (prop === "unwrap") {
719
+ return () => target;
720
+ }
721
+ return Reflect.get(target, prop, receiver);
722
+ }
723
+ };
724
+ return new Proxy(client, handler);
725
+ }
726
+ function streamOpenAIRoute(usageTap, openai, options) {
727
+ if (!options?.getRequest) {
728
+ throw new UsageTapError("USAGETAP_BAD_REQUEST", "streamOpenAIRoute requires a getRequest function");
729
+ }
730
+ const wrapConfig = options.wrapOptions || options.defaultContext ? {
731
+ ...options.wrapOptions ?? {},
732
+ defaultContext: options.defaultContext ?? options.wrapOptions?.defaultContext
733
+ } : void 0;
734
+ const wrappedClient = wrapConfig ? wrapOpenAI(openai, usageTap, wrapConfig) : wrapOpenAI(openai, usageTap);
735
+ return async (req) => {
736
+ const requestConfig = await options.getRequest(req);
737
+ const mergedParams = {
738
+ ...requestConfig.params,
739
+ stream: true
740
+ };
741
+ const callOptions = {};
742
+ if (requestConfig.usageTap) {
743
+ callOptions.usageTap = requestConfig.usageTap;
744
+ }
745
+ if (requestConfig.withUsage) {
746
+ callOptions.withUsage = requestConfig.withUsage;
747
+ }
748
+ const stream = await wrappedClient.chat.completions.create(
749
+ mergedParams,
750
+ Object.keys(callOptions).length ? callOptions : void 0
751
+ );
752
+ const baseResponse = toNextResponse(stream, {
753
+ mode: options.stream?.mode ?? "sse",
754
+ headers: options.stream?.headers
755
+ });
756
+ const init = options.stream?.responseInit;
757
+ if (!init) {
758
+ return baseResponse;
759
+ }
760
+ const mergedHeaders = new Headers(baseResponse.headers);
761
+ if (init.headers) {
762
+ const extra = normalizeHeaders(init.headers);
763
+ for (const [key, value] of Object.entries(extra)) {
764
+ mergedHeaders.set(key, value);
765
+ }
766
+ }
767
+ return new Response(baseResponse.body, {
768
+ status: init.status ?? baseResponse.status,
769
+ statusText: init.statusText ?? baseResponse.statusText,
770
+ headers: mergedHeaders
771
+ });
772
+ };
773
+ }
774
+ function createChatProxy(resource, usageTap, defaultContext, applyVendorHints) {
775
+ const completions = createChatCompletionsProxy(
776
+ resource.completions,
777
+ usageTap,
778
+ defaultContext,
779
+ applyVendorHints
780
+ );
781
+ const handler = {
782
+ get(target, prop, receiver) {
783
+ if (prop === "completions") {
784
+ return completions;
785
+ }
786
+ return Reflect.get(target, prop, receiver);
787
+ }
788
+ };
789
+ return new Proxy(resource, handler);
790
+ }
791
+ function createResponsesProxy(resource, usageTap, defaultContext, applyVendorHints) {
792
+ if (!resource || typeof resource !== "object") {
793
+ return void 0;
794
+ }
795
+ if (!("create" in resource) || typeof resource.create !== "function") {
796
+ return resource;
797
+ }
798
+ const originalCreate = resource.create.bind(resource);
799
+ const wrappedCreate = (params, options) => {
800
+ const { requestOptions, usageContext, withUsage: withUsage2 } = splitUsageOptions(options);
801
+ const beginRequest = resolveBeginRequest(defaultContext, usageContext);
802
+ const wantsStream = isStreamingRequest(params);
803
+ return usageTap.withUsage(beginRequest, (ctx) => {
804
+ const finalParams = applyVendorHints ? applyResponsesVendorHints(params, ctx.begin.data.vendorHints) : params;
805
+ const request = attachCorrelationHeader(requestOptions, ctx.begin.correlationId);
806
+ if (wantsStream) {
807
+ const apiPromise2 = originalCreate(finalParams, request);
808
+ const wrappedPromise2 = transformApiPromise(apiPromise2, (rawStream) => {
809
+ ensureAsyncIterable(rawStream, "responses.create");
810
+ const wrappedStream = wrapStreamForUsageTap(rawStream, async () => {
811
+ const usage = await extractUsageFromStream(rawStream, ctx.begin.data.vendorHints);
812
+ if (usage) {
813
+ ctx.setUsage(usage);
814
+ }
815
+ }, ctx);
816
+ return wrappedStream;
817
+ });
818
+ return wrappedPromise2;
819
+ }
820
+ const apiPromise = originalCreate(finalParams, request);
821
+ const wrappedPromise = transformApiPromise(apiPromise, (response) => {
822
+ tryInferUsage(response, ctx.begin.data.vendorHints, void 0, ctx);
823
+ return response;
824
+ });
825
+ return wrappedPromise;
826
+ }, withUsage2);
827
+ };
828
+ const handler = {
829
+ get(target, prop, receiver) {
830
+ if (prop === "create") {
831
+ return wrappedCreate;
832
+ }
833
+ return Reflect.get(target, prop, receiver);
834
+ }
835
+ };
836
+ return new Proxy(resource, handler);
837
+ }
838
+ function createChatCompletionsProxy(resource, usageTap, defaultContext, applyVendorHints) {
839
+ const originalCreate = resource.create.bind(resource);
840
+ const streamCandidate = resource.stream;
841
+ const originalStream = typeof streamCandidate === "function" ? streamCandidate.bind(resource) : void 0;
842
+ const wrappedCreate = (params, options) => {
843
+ const { requestOptions, usageContext, withUsage: withUsage2 } = splitUsageOptions(options);
844
+ const beginRequest = resolveBeginRequest(defaultContext, usageContext);
845
+ const wantsStream = isStreamingRequest(params);
846
+ return usageTap.withUsage(beginRequest, (ctx) => {
847
+ const finalParams = applyVendorHints ? applyChatVendorHints(params, ctx.begin.data.vendorHints) : params;
848
+ const request = attachCorrelationHeader(requestOptions, ctx.begin.correlationId);
849
+ if (wantsStream) {
850
+ const apiPromise2 = originalCreate(finalParams, request);
851
+ const wrappedPromise2 = transformApiPromise(apiPromise2, (rawStream) => {
852
+ ensureAsyncIterable(rawStream, "chat.completions.create");
853
+ const wrappedStream2 = wrapStreamForUsageTap(rawStream, async () => {
854
+ const usage = await extractUsageFromStream(rawStream, ctx.begin.data.vendorHints);
855
+ if (usage) {
856
+ ctx.setUsage(usage);
857
+ }
858
+ }, ctx);
859
+ return wrappedStream2;
860
+ });
861
+ return wrappedPromise2;
862
+ }
863
+ const apiPromise = originalCreate(finalParams, request);
864
+ const wrappedPromise = transformApiPromise(apiPromise, (response) => {
865
+ tryInferUsage(response, ctx.begin.data.vendorHints, void 0, ctx);
866
+ return response;
867
+ });
868
+ return wrappedPromise;
869
+ }, withUsage2);
870
+ };
871
+ const wrappedStream = originalStream ? (params, options) => {
872
+ const { requestOptions, usageContext, withUsage: withUsage2 } = splitUsageOptions(options);
873
+ const beginRequest = resolveBeginRequest(defaultContext, usageContext);
874
+ return usageTap.withUsage(beginRequest, (ctx) => {
875
+ const finalParams = applyVendorHints ? applyChatVendorHints(params, ctx.begin.data.vendorHints) : params;
876
+ const request = attachCorrelationHeader(requestOptions, ctx.begin.correlationId);
877
+ const apiPromise = originalStream(finalParams, request);
878
+ const wrappedPromise = transformApiPromise(apiPromise, (rawStream) => {
879
+ ensureAsyncIterable(rawStream, "chat.completions.stream");
880
+ const wrappedStreamInner = wrapStreamForUsageTap(rawStream, async () => {
881
+ const usage = await extractUsageFromStream(rawStream, ctx.begin.data.vendorHints);
882
+ if (usage) {
883
+ ctx.setUsage(usage);
884
+ }
885
+ }, ctx);
886
+ return wrappedStreamInner;
887
+ });
888
+ return wrappedPromise;
889
+ }, withUsage2);
890
+ } : void 0;
891
+ const handler = {
892
+ get(target, prop, receiver) {
893
+ if (prop === "create") {
894
+ return wrappedCreate;
895
+ }
896
+ if (prop === "stream" && wrappedStream) {
897
+ return wrappedStream;
898
+ }
899
+ return Reflect.get(target, prop, receiver);
900
+ }
901
+ };
902
+ return new Proxy(resource, handler);
903
+ }
904
+ function splitUsageOptions(options) {
905
+ if (!options || typeof options !== "object") {
906
+ return {};
907
+ }
908
+ const { usageTap, withUsage: withUsage2, ...rest } = options;
909
+ const requestOptions = Object.keys(rest).length ? cloneRequestOptions(rest) : void 0;
910
+ return {
911
+ requestOptions,
912
+ usageContext: usageTap,
913
+ withUsage: withUsage2
914
+ };
915
+ }
916
+ function resolveBeginRequest(defaults, override) {
917
+ const base = defaults ?? {};
918
+ const current = override ?? {};
919
+ const customerId = current.customerId ?? base.customerId;
920
+ if (!customerId) {
921
+ throw new UsageTapError(
922
+ "USAGETAP_BAD_REQUEST",
923
+ "wrapOpenAI requires usageTap.customerId (provide defaultContext or options.usageTap)"
924
+ );
925
+ }
926
+ const tags = mergeTags(base.tags, current.tags);
927
+ const begin = { customerId };
928
+ const requested = current.requested ?? base.requested;
929
+ if (requested) begin.requested = requested;
930
+ const feature = current.feature ?? base.feature;
931
+ if (feature) begin.feature = feature;
932
+ const idempotency = current.idempotency ?? base.idempotency;
933
+ if (idempotency) begin.idempotency = idempotency;
934
+ const customerName = current.customerName ?? base.customerName;
935
+ if (customerName) begin.customerName = customerName;
936
+ const customerEmail = current.customerEmail ?? base.customerEmail;
937
+ if (customerEmail) begin.customerEmail = customerEmail;
938
+ if (tags?.length) {
939
+ begin.tags = tags;
940
+ }
941
+ return begin;
942
+ }
943
+ function transformApiPromise(apiPromise, onResolve) {
944
+ const resolvedPromise = Promise.resolve(apiPromise).then(onResolve);
945
+ if (isObjectRecord(apiPromise)) {
946
+ const proto = Object.getPrototypeOf(apiPromise);
947
+ if (proto) {
948
+ Object.setPrototypeOf(resolvedPromise, proto);
949
+ }
950
+ for (const key of Reflect.ownKeys(apiPromise)) {
951
+ if (key === "then" || key === "catch" || key === "finally") {
952
+ continue;
953
+ }
954
+ try {
955
+ const descriptor = Object.getOwnPropertyDescriptor(apiPromise, key);
956
+ if (descriptor) {
957
+ Reflect.defineProperty(resolvedPromise, key, descriptor);
958
+ }
959
+ } catch {
960
+ }
961
+ }
962
+ }
963
+ return resolvedPromise;
964
+ }
965
+ function isObjectRecord(value) {
966
+ return typeof value === "object" && value !== null;
967
+ }
968
+ function cloneRecord(value) {
969
+ return isObjectRecord(value) ? { ...value } : {};
970
+ }
971
+ function isStringTuple(value) {
972
+ return Array.isArray(value) && value.length >= 2 && typeof value[0] === "string" && typeof value[1] === "string";
973
+ }
974
+ function cloneRequestOptions(source) {
975
+ const clone = { ...source };
976
+ if ("headers" in clone) {
977
+ clone.headers = normalizeHeaders(clone.headers);
978
+ }
979
+ return clone;
980
+ }
981
+ function attachCorrelationHeader(options, correlationId) {
982
+ const normalized = normalizeHeaders(options?.headers);
983
+ if (correlationId && !normalized[USAGETAP_CORRELATION_HEADER]) {
984
+ normalized[USAGETAP_CORRELATION_HEADER] = correlationId;
985
+ }
986
+ if (!options) {
987
+ return Object.keys(normalized).length ? { headers: normalized } : void 0;
988
+ }
989
+ const next = { ...options };
990
+ if (Object.keys(normalized).length) {
991
+ next.headers = normalized;
992
+ }
993
+ return next;
994
+ }
995
+ function normalizeHeaders(headers) {
996
+ if (!headers) {
997
+ return {};
998
+ }
999
+ if (headers instanceof Headers) {
1000
+ const result = {};
1001
+ headers.forEach((value, key) => {
1002
+ result[key.toLowerCase()] = value;
1003
+ });
1004
+ return result;
1005
+ }
1006
+ if (Array.isArray(headers)) {
1007
+ const result = {};
1008
+ for (const entry of headers) {
1009
+ if (!isStringTuple(entry)) {
1010
+ continue;
1011
+ }
1012
+ const [key, value] = entry;
1013
+ result[key.toLowerCase()] = value;
1014
+ }
1015
+ return result;
1016
+ }
1017
+ if (isObjectRecord(headers)) {
1018
+ const result = {};
1019
+ const record = headers;
1020
+ for (const key of Object.keys(record)) {
1021
+ const value = record[key];
1022
+ if (value !== void 0 && value !== null) {
1023
+ result[key.toLowerCase()] = String(value);
1024
+ }
1025
+ }
1026
+ return result;
1027
+ }
1028
+ return {};
1029
+ }
1030
+ function mergeTags(a, b) {
1031
+ const values = [...a ?? [], ...b ?? []].map((value) => typeof value === "string" ? value.trim() : "").filter(Boolean);
1032
+ if (!values.length) {
1033
+ return void 0;
1034
+ }
1035
+ return dedupeStrings2(values);
1036
+ }
1037
+ function dedupeStrings2(values) {
1038
+ return Array.from(new Set(values));
1039
+ }
1040
+ function isStreamingRequest(params) {
1041
+ if (!params || typeof params !== "object") {
1042
+ return false;
1043
+ }
1044
+ const stream = params.stream;
1045
+ if (typeof stream === "boolean") {
1046
+ return stream;
1047
+ }
1048
+ return stream != null;
1049
+ }
1050
+ function applyChatVendorHints(params, hints) {
1051
+ if (!hints) {
1052
+ return params;
1053
+ }
1054
+ const next = cloneRecord(params);
1055
+ if (hints.preferredModel && (next.model === void 0 || next.model === null)) {
1056
+ next.model = hints.preferredModel;
1057
+ }
1058
+ if (typeof hints.maxResponseTokens === "number" && next.max_tokens == null) {
1059
+ next.max_tokens = hints.maxResponseTokens;
1060
+ }
1061
+ if (typeof hints.maxInputTokens === "number" && next.max_input_tokens == null) {
1062
+ next.max_input_tokens = hints.maxInputTokens;
1063
+ }
1064
+ return next;
1065
+ }
1066
+ function applyResponsesVendorHints(params, hints) {
1067
+ if (!hints) {
1068
+ return params;
1069
+ }
1070
+ const next = cloneRecord(params);
1071
+ if (hints.preferredModel && (next.model === void 0 || next.model === null)) {
1072
+ next.model = hints.preferredModel;
1073
+ }
1074
+ if (typeof hints.maxResponseTokens === "number" && next.max_output_tokens == null) {
1075
+ next.max_output_tokens = hints.maxResponseTokens;
1076
+ }
1077
+ return next;
1078
+ }
1079
+ async function extractUsageFromStream(stream, hints) {
1080
+ const finalPayload = await resolveStreamFinalPayload(stream);
1081
+ if (!finalPayload) {
1082
+ return void 0;
1083
+ }
1084
+ return inferUsageFromResponse(finalPayload, hints);
1085
+ }
1086
+ async function resolveStreamFinalPayload(stream) {
1087
+ if (!stream || typeof stream !== "object") {
1088
+ return void 0;
1089
+ }
1090
+ const candidate = stream;
1091
+ if (typeof candidate.finalChatCompletion === "function") {
1092
+ return candidate.finalChatCompletion();
1093
+ }
1094
+ if (typeof candidate.finalResponse === "function") {
1095
+ return candidate.finalResponse();
1096
+ }
1097
+ if (typeof candidate.finalCompletion === "function") {
1098
+ return candidate.finalCompletion();
1099
+ }
1100
+ if (typeof candidate.finalContent === "function") {
1101
+ return candidate.finalContent();
1102
+ }
1103
+ return void 0;
1104
+ }
1105
+ function ensureAsyncIterable(value, label) {
1106
+ if (!value || typeof value !== "object" || typeof value[Symbol.asyncIterator] !== "function") {
1107
+ throw new UsageTapError(
1108
+ "USAGETAP_BAD_REQUEST",
1109
+ `${label} expected an async iterable stream but received ${typeof value}`
1110
+ );
1111
+ }
1112
+ }
1113
+ function chunkToText(chunk) {
1114
+ if (chunk === void 0 || chunk === null) {
1115
+ return "";
1116
+ }
1117
+ if (typeof chunk === "string") {
1118
+ return chunk;
1119
+ }
1120
+ if (typeof chunk === "object") {
1121
+ const candidate = chunk;
1122
+ const delta = candidate.choices?.[0]?.delta;
1123
+ const content = delta?.content ?? candidate.content;
1124
+ if (typeof content === "string") {
1125
+ return content;
1126
+ }
1127
+ if (Array.isArray(content)) {
1128
+ return content.map((entry) => {
1129
+ if (!entry) return "";
1130
+ if (typeof entry === "string") return entry;
1131
+ if (typeof entry.text === "string") return entry.text;
1132
+ return "";
1133
+ }).join("");
1134
+ }
1135
+ }
1136
+ return String(chunk);
1137
+ }
1138
+ function formatSsePayload(text, options) {
1139
+ if (!text) {
1140
+ return "";
1141
+ }
1142
+ const lines = text.split(/\r?\n/);
1143
+ const eventLine = options?.event ? `event: ${options.event}
1144
+ ` : "";
1145
+ const retryLine = options?.retry ? `retry: ${options.retry}
1146
+ ` : "";
1147
+ const dataLines = lines.map((line) => `data: ${line}`).join("\n");
1148
+ return `${eventLine}${retryLine}${dataLines}
1149
+
1150
+ `;
1151
+ }
1152
+ function setHeaderIfPossible(res, key, value) {
1153
+ if (typeof res.setHeader === "function" && res.headersSent !== true) {
1154
+ res.setHeader(key, value);
1155
+ }
1156
+ }
1157
+ function tryInferUsage(response, hints, extractor, ctx) {
1158
+ const explicit = extractor?.(response);
1159
+ const inferred = explicit ?? inferUsageFromResponse(response, hints);
1160
+ if (inferred) {
1161
+ ctx.setUsage(inferred);
1162
+ }
1163
+ }
1164
+ function inferUsageFromResponse(response, hints) {
1165
+ if (!response || typeof response !== "object") {
1166
+ return void 0;
1167
+ }
1168
+ const candidate = response;
1169
+ if (!candidate.usage) {
1170
+ return void 0;
1171
+ }
1172
+ return {
1173
+ modelUsed: candidate.model ?? hints?.preferredModel,
1174
+ inputTokens: candidate.usage.prompt_tokens,
1175
+ responseTokens: candidate.usage.completion_tokens,
1176
+ cachedTokens: candidate.usage.cached_tokens
1177
+ };
1178
+ }
1179
+ function wrapStreamForUsageTap(source, finalize, ctx) {
1180
+ const getIterator = source[Symbol.asyncIterator];
1181
+ if (typeof getIterator !== "function") {
1182
+ throw new TypeError("Stream is not async iterable");
1183
+ }
1184
+ const iterator = getIterator.call(source);
1185
+ let completed = false;
1186
+ const invokeFinalize = async () => {
1187
+ if (completed) return;
1188
+ completed = true;
1189
+ try {
1190
+ await finalize();
1191
+ } catch (error) {
1192
+ ctx.setError({
1193
+ code: "USAGE_FINALIZE_ERROR",
1194
+ message: error instanceof Error ? error.message : String(error)
1195
+ });
1196
+ throw error;
1197
+ }
1198
+ };
1199
+ const prototype = Object.getPrototypeOf(source) ?? Object.prototype;
1200
+ const wrapped = Object.create(prototype);
1201
+ for (const key of Reflect.ownKeys(source)) {
1202
+ try {
1203
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
1204
+ if (descriptor) {
1205
+ Object.defineProperty(wrapped, key, descriptor);
1206
+ }
1207
+ } catch {
1208
+ }
1209
+ }
1210
+ Object.defineProperty(wrapped, Symbol.asyncIterator, {
1211
+ value() {
1212
+ return this;
1213
+ },
1214
+ configurable: true
1215
+ });
1216
+ Object.defineProperty(wrapped, "next", {
1217
+ value: async (...args) => {
1218
+ try {
1219
+ const result = await iterator.next(...args);
1220
+ if (result.done) {
1221
+ await invokeFinalize();
1222
+ }
1223
+ return result;
1224
+ } catch (error) {
1225
+ await invokeFinalize().catch(() => void 0);
1226
+ throw error;
1227
+ }
1228
+ },
1229
+ configurable: true,
1230
+ writable: true
1231
+ });
1232
+ Object.defineProperty(wrapped, "return", {
1233
+ value: async (value) => {
1234
+ if (typeof iterator.return === "function") {
1235
+ const rawResult = await iterator.return(value);
1236
+ if (!isIteratorResult(rawResult)) {
1237
+ throw new TypeError("Iterator.return() returned an invalid result");
1238
+ }
1239
+ await invokeFinalize();
1240
+ return rawResult;
1241
+ }
1242
+ await invokeFinalize();
1243
+ return { done: true, value };
1244
+ },
1245
+ configurable: true,
1246
+ writable: true
1247
+ });
1248
+ Object.defineProperty(wrapped, "throw", {
1249
+ value: async (error) => {
1250
+ if (typeof iterator.throw === "function") {
1251
+ const rawResult = await iterator.throw(error);
1252
+ if (!isIteratorResult(rawResult)) {
1253
+ throw new TypeError("Iterator.throw() returned an invalid result");
1254
+ }
1255
+ await invokeFinalize();
1256
+ return rawResult;
1257
+ }
1258
+ await invokeFinalize();
1259
+ throw error;
1260
+ },
1261
+ configurable: true,
1262
+ writable: true
1263
+ });
1264
+ Object.defineProperty(wrapped, "__usageTapFinalize", {
1265
+ value: async () => {
1266
+ await invokeFinalize();
1267
+ },
1268
+ configurable: true
1269
+ });
1270
+ return wrapped;
1271
+ }
1272
+ function isIteratorResult(value) {
1273
+ return isObjectRecord(value) && "done" in value;
1274
+ }
1275
+
1276
+ // src/adapters/openrouter.ts
1277
+ function createOpenRouterAdapter(init) {
1278
+ return createOpenAIAdapter(init);
1279
+ }
1280
+
1281
+ // src/adapters/fetch-wrapper.ts
1282
+ function isJsonRecord(value) {
1283
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1284
+ }
1285
+ function readString(value) {
1286
+ return typeof value === "string" ? value : void 0;
1287
+ }
1288
+ function readNumber(value) {
1289
+ return typeof value === "number" ? value : void 0;
1290
+ }
1291
+ function parseJsonRecord(text) {
1292
+ try {
1293
+ const parsed = JSON.parse(text);
1294
+ return isJsonRecord(parsed) ? parsed : void 0;
1295
+ } catch {
1296
+ return void 0;
1297
+ }
1298
+ }
1299
+ function parseStringArray(value) {
1300
+ try {
1301
+ const parsed = JSON.parse(value);
1302
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
1303
+ return parsed;
1304
+ }
1305
+ } catch {
1306
+ return void 0;
1307
+ }
1308
+ return void 0;
1309
+ }
1310
+ function wrapFetch(usageTap, options) {
1311
+ const {
1312
+ defaultContext,
1313
+ baseFetch = globalThis.fetch,
1314
+ autoIdempotency = true,
1315
+ isOpenAIEndpoint = defaultIsOpenAIEndpoint
1316
+ } = options;
1317
+ return async function wrappedFetch(input, init) {
1318
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1319
+ if (!isOpenAIEndpoint(url)) {
1320
+ return baseFetch(input, init);
1321
+ }
1322
+ let body;
1323
+ let isStreaming = false;
1324
+ const contextOverride = {};
1325
+ try {
1326
+ if (init?.body && typeof init.body === "string") {
1327
+ const parsedBody = parseJsonRecord(init.body);
1328
+ if (!parsedBody) {
1329
+ return baseFetch(input, init);
1330
+ }
1331
+ body = parsedBody;
1332
+ isStreaming = parsedBody.stream === true;
1333
+ const headers = new Headers(init.headers);
1334
+ const customerIdHeader = headers.get("x-usagetap-customer-id");
1335
+ const featureHeader = headers.get("x-usagetap-feature");
1336
+ const tagsHeader = headers.get("x-usagetap-tags");
1337
+ if (customerIdHeader) {
1338
+ contextOverride.customerId = customerIdHeader;
1339
+ }
1340
+ if (featureHeader) {
1341
+ contextOverride.feature = featureHeader;
1342
+ }
1343
+ if (tagsHeader) {
1344
+ const tags = parseStringArray(tagsHeader);
1345
+ if (tags) {
1346
+ contextOverride.tags = tags;
1347
+ }
1348
+ }
1349
+ }
1350
+ } catch {
1351
+ return baseFetch(input, init);
1352
+ }
1353
+ const context = {
1354
+ ...defaultContext,
1355
+ ...contextOverride,
1356
+ idempotency: autoIdempotency ? crypto.randomUUID() : void 0
1357
+ };
1358
+ let callState;
1359
+ try {
1360
+ const beginResponse = await usageTap.beginCall(context);
1361
+ callState = {
1362
+ callId: beginResponse.data.callId,
1363
+ correlationId: beginResponse.correlationId,
1364
+ usage: {},
1365
+ finalized: false
1366
+ };
1367
+ } catch (error) {
1368
+ console.error("[wrapFetch] Failed to begin call:", error);
1369
+ return baseFetch(input, init);
1370
+ }
1371
+ const modifiedInit = {
1372
+ ...init,
1373
+ headers: {
1374
+ ...init?.headers,
1375
+ "x-usage-correlation-id": callState.correlationId || ""
1376
+ }
1377
+ };
1378
+ try {
1379
+ const response = await baseFetch(input, modifiedInit);
1380
+ if (isStreaming) {
1381
+ return wrapStreamingResponse(response, callState, usageTap);
1382
+ } else {
1383
+ return await wrapNonStreamingResponse(response, callState, usageTap, body);
1384
+ }
1385
+ } catch (error) {
1386
+ const message = error instanceof Error ? error.message : String(error);
1387
+ await finalizeCall(callState, usageTap, {
1388
+ code: "VENDOR_ERROR",
1389
+ message
1390
+ });
1391
+ throw error;
1392
+ }
1393
+ };
1394
+ }
1395
+ function defaultIsOpenAIEndpoint(url) {
1396
+ return url.includes("/v1/chat/completions") || url.includes("/v1/responses") || url.includes("/v1/embeddings");
1397
+ }
1398
+ async function wrapNonStreamingResponse(response, callState, usageTap, requestBody) {
1399
+ const clonedResponse = response.clone();
1400
+ try {
1401
+ const parsed = await clonedResponse.json();
1402
+ const usage = {};
1403
+ if (isJsonRecord(parsed)) {
1404
+ const usageBlock = parsed.usage;
1405
+ if (isJsonRecord(usageBlock)) {
1406
+ const promptTokens = readNumber(usageBlock.prompt_tokens ?? usageBlock.input_tokens);
1407
+ if (promptTokens !== void 0) {
1408
+ usage.inputTokens = promptTokens;
1409
+ }
1410
+ const completionTokens = readNumber(usageBlock.completion_tokens ?? usageBlock.output_tokens);
1411
+ if (completionTokens !== void 0) {
1412
+ usage.responseTokens = completionTokens;
1413
+ }
1414
+ const cachedTokens = readNumber(usageBlock.prompt_cache_hit_tokens);
1415
+ if (cachedTokens !== void 0) {
1416
+ usage.cachedTokens = cachedTokens;
1417
+ }
1418
+ const reasoningTokens = readNumber(usageBlock.reasoning_tokens);
1419
+ if (reasoningTokens !== void 0) {
1420
+ usage.reasoningTokens = reasoningTokens;
1421
+ }
1422
+ }
1423
+ const modelFromResponse = readString(parsed.model);
1424
+ if (modelFromResponse) {
1425
+ usage.modelUsed = modelFromResponse;
1426
+ }
1427
+ }
1428
+ if (!usage.modelUsed && requestBody) {
1429
+ const requestModel = readString(requestBody.model);
1430
+ if (requestModel) {
1431
+ usage.modelUsed = requestModel;
1432
+ }
1433
+ }
1434
+ await finalizeCall(callState, usageTap, void 0, usage);
1435
+ return response;
1436
+ } catch (error) {
1437
+ const message = error instanceof Error ? error.message : String(error);
1438
+ await finalizeCall(callState, usageTap, {
1439
+ code: "RESPONSE_PARSE_ERROR",
1440
+ message
1441
+ });
1442
+ return response;
1443
+ }
1444
+ }
1445
+ function wrapStreamingResponse(response, callState, usageTap) {
1446
+ if (!response.body) {
1447
+ void finalizeCall(callState, usageTap, {
1448
+ code: "NO_RESPONSE_BODY",
1449
+ message: "Streaming response has no body"
1450
+ });
1451
+ return response;
1452
+ }
1453
+ const originalBody = response.body;
1454
+ const accumulatedUsage = {};
1455
+ const textDecoder = new TextDecoder();
1456
+ const transformStream = new TransformStream({
1457
+ transform(chunk, controller) {
1458
+ controller.enqueue(chunk);
1459
+ try {
1460
+ const text = textDecoder.decode(chunk, { stream: true });
1461
+ const lines = text.split("\n");
1462
+ for (const line of lines) {
1463
+ if (line.startsWith("data: ")) {
1464
+ const data = line.slice(6);
1465
+ if (data === "[DONE]") continue;
1466
+ const parsedRecord = parseJsonRecord(data);
1467
+ if (!parsedRecord) {
1468
+ continue;
1469
+ }
1470
+ const usageBlock = parsedRecord.usage;
1471
+ if (isJsonRecord(usageBlock)) {
1472
+ const promptTokens = readNumber(usageBlock.prompt_tokens ?? usageBlock.input_tokens);
1473
+ if (promptTokens !== void 0) {
1474
+ accumulatedUsage.inputTokens = promptTokens;
1475
+ }
1476
+ const completionTokens = readNumber(usageBlock.completion_tokens ?? usageBlock.output_tokens);
1477
+ if (completionTokens !== void 0) {
1478
+ accumulatedUsage.responseTokens = completionTokens;
1479
+ }
1480
+ const cachedTokens = readNumber(usageBlock.prompt_cache_hit_tokens);
1481
+ if (cachedTokens !== void 0) {
1482
+ accumulatedUsage.cachedTokens = cachedTokens;
1483
+ }
1484
+ const reasoningTokens = readNumber(usageBlock.reasoning_tokens);
1485
+ if (reasoningTokens !== void 0) {
1486
+ accumulatedUsage.reasoningTokens = reasoningTokens;
1487
+ }
1488
+ }
1489
+ const model = readString(parsedRecord.model);
1490
+ if (model) {
1491
+ accumulatedUsage.modelUsed = model;
1492
+ }
1493
+ }
1494
+ }
1495
+ } catch {
1496
+ }
1497
+ },
1498
+ async flush() {
1499
+ await finalizeCall(callState, usageTap, void 0, accumulatedUsage);
1500
+ }
1501
+ });
1502
+ const wrappedBody = originalBody.pipeThrough(transformStream);
1503
+ return new Response(wrappedBody, {
1504
+ status: response.status,
1505
+ statusText: response.statusText,
1506
+ headers: response.headers
1507
+ });
1508
+ }
1509
+ async function finalizeCall(callState, usageTap, error, usage) {
1510
+ if (callState.finalized) return;
1511
+ callState.finalized = true;
1512
+ try {
1513
+ await usageTap.endCall({
1514
+ callId: callState.callId,
1515
+ ...callState.usage,
1516
+ ...usage,
1517
+ error
1518
+ });
1519
+ } catch (err) {
1520
+ console.error("[wrapFetch] Failed to finalize call:", err);
1521
+ }
1522
+ }
1523
+
1524
+ // src/adapters/express-middleware.ts
1525
+ function withUsageMiddleware(usageTap, options) {
1526
+ return async (req, res, next) => {
1527
+ try {
1528
+ const customerId = await options.getCustomerId(req);
1529
+ const feature = options.getFeature ? await options.getFeature(req) : void 0;
1530
+ const tags = options.getTags ? await options.getTags(req) : void 0;
1531
+ const baseContext = {
1532
+ customerId,
1533
+ feature,
1534
+ tags,
1535
+ ...options.defaultContext
1536
+ };
1537
+ req.usageTap = {
1538
+ openai: (client, contextOverride, wrapOptions) => {
1539
+ const mergedContext = { ...baseContext, ...contextOverride };
1540
+ return wrapOpenAI(client, usageTap, {
1541
+ ...wrapOptions,
1542
+ defaultContext: mergedContext
1543
+ });
1544
+ },
1545
+ pipeToResponse
1546
+ };
1547
+ next();
1548
+ } catch (error) {
1549
+ next(error);
1550
+ }
1551
+ };
1552
+ }
1553
+ function withUsage(usageTap, getCustomerId) {
1554
+ return withUsageMiddleware(usageTap, { getCustomerId });
1555
+ }
1556
+
1557
+ export { UsageTapClient, UsageTapError, createIdempotencyKey, createOpenAIAdapter, createOpenRouterAdapter, isUsageTapError, pipeToResponse, streamOpenAIRoute, toNextResponse, withUsage, withUsageMiddleware, wrapFetch, wrapOpenAI };
1558
+ //# sourceMappingURL=index.js.map
1559
+ //# sourceMappingURL=index.js.map