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