@usagetap/sdk 1.3.2 → 1.7.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 +372 -39
- package/dist/adapters/anthropic.cjs +995 -69
- package/dist/adapters/anthropic.cjs.map +1 -1
- package/dist/adapters/anthropic.d.cts +45 -3
- package/dist/adapters/anthropic.d.ts +45 -3
- package/dist/adapters/anthropic.mjs +995 -70
- package/dist/adapters/anthropic.mjs.map +1 -1
- package/dist/adapters/openai.cjs +1208 -106
- package/dist/adapters/openai.cjs.map +1 -1
- package/dist/adapters/openai.d.cts +46 -3
- package/dist/adapters/openai.d.ts +46 -3
- package/dist/adapters/openai.mjs +1208 -107
- package/dist/adapters/openai.mjs.map +1 -1
- package/dist/adapters/openrouter.cjs +3912 -53
- package/dist/adapters/openrouter.cjs.map +1 -1
- package/dist/adapters/openrouter.d.cts +6 -3
- package/dist/adapters/openrouter.d.ts +6 -3
- package/dist/adapters/openrouter.mjs +3910 -54
- package/dist/adapters/openrouter.mjs.map +1 -1
- package/dist/anthropic/index.cjs +995 -69
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.d.cts +2 -2
- package/dist/anthropic/index.d.ts +2 -2
- package/dist/anthropic/index.mjs +995 -70
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/client-C0UiaqVB.d.cts +1305 -0
- package/dist/client-C0UiaqVB.d.ts +1305 -0
- package/dist/express/index.cjs +399 -64
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +2 -2
- package/dist/express/index.d.ts +2 -2
- package/dist/express/index.mjs +399 -64
- package/dist/express/index.mjs.map +1 -1
- package/dist/index.cjs +1044 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -5
- package/dist/index.d.ts +16 -5
- package/dist/index.mjs +1044 -163
- package/dist/index.mjs.map +1 -1
- package/dist/openai/index.cjs +1209 -107
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.d.cts +2 -2
- package/dist/openai/index.d.ts +2 -2
- package/dist/openai/index.mjs +1209 -108
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openrouter/index.cjs +1226 -109
- package/dist/openrouter/index.cjs.map +1 -1
- package/dist/openrouter/index.d.cts +3 -3
- package/dist/openrouter/index.d.ts +3 -3
- package/dist/openrouter/index.mjs +1224 -108
- package/dist/openrouter/index.mjs.map +1 -1
- package/dist/react/index.cjs +19 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +17 -4
- package/dist/react/index.d.ts +17 -4
- package/dist/react/index.mjs +19 -1
- package/dist/react/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/client-BD8O2J8Z.d.cts +0 -668
- package/dist/client-BD8O2J8Z.d.ts +0 -668
|
@@ -1,16 +1,2273 @@
|
|
|
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/prompt-compression.ts
|
|
117
|
+
var DEFAULT_TTC_ENDPOINT = "https://api.thetokencompany.com/v1/compress";
|
|
118
|
+
var DEFAULT_TTC_MODEL = "bear-2";
|
|
119
|
+
var DEFAULT_TTC_AGGRESSIVENESS = 0.2;
|
|
120
|
+
var DEFAULT_USAGETAP_COMPRESSION_ENDPOINT = "https://compress.usagetap.com/v1/compress";
|
|
121
|
+
var DEFAULT_USAGETAP_MESSAGES_COMPRESSION_ENDPOINT = "https://compress.usagetap.com/v1/messages/compress";
|
|
122
|
+
var PROTECTED_TEXT_PATTERN = /<ttc_safe>[\s\S]*?<\/ttc_safe>|<usagetap_safe>[\s\S]*?<\/usagetap_safe>/g;
|
|
123
|
+
async function compressPrompt(options) {
|
|
124
|
+
const input = resolvePromptCompressionInput(options);
|
|
125
|
+
try {
|
|
126
|
+
if (options.provider === "usagetap") {
|
|
127
|
+
return await compressWithUsageTap(options);
|
|
128
|
+
}
|
|
129
|
+
if (options.provider === "thetokencompany" || options.tokenCompanyApiKey) {
|
|
130
|
+
return await compressWithTheTokenCompany(options);
|
|
131
|
+
}
|
|
132
|
+
if (options.provider === "toon") {
|
|
133
|
+
return compressPromptToon(input);
|
|
134
|
+
}
|
|
135
|
+
return compressPromptHeuristic(input);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (options.failOpen === false) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
return createPromptCompressionFallback(
|
|
141
|
+
input,
|
|
142
|
+
options.provider ?? (options.tokenCompanyApiKey ? "thetokencompany" : "heuristic"),
|
|
143
|
+
error
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function compressPromptHeuristic(input) {
|
|
148
|
+
const original = stableStringifyInput(input);
|
|
149
|
+
const techniques = /* @__PURE__ */ new Set();
|
|
150
|
+
const compressedInput = compressValue(input, techniques, { allowToonString: false });
|
|
151
|
+
const compressed = stableStringifyInput(compressedInput);
|
|
152
|
+
const chosenInput = compressed.length <= original.length ? compressedInput : input;
|
|
153
|
+
const chosen = compressed.length <= original.length ? compressed : original;
|
|
154
|
+
if (!techniques.size) {
|
|
155
|
+
techniques.add("no-op");
|
|
156
|
+
}
|
|
157
|
+
return buildResult(
|
|
158
|
+
input,
|
|
159
|
+
chosenInput,
|
|
160
|
+
"heuristic",
|
|
161
|
+
original,
|
|
162
|
+
chosen,
|
|
163
|
+
Array.from(techniques)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function compressPromptToon(input) {
|
|
167
|
+
const original = stableStringifyInput(input);
|
|
168
|
+
const compressedInput = typeof input === "string" ? compressText(input, /* @__PURE__ */ new Set(), { allowToonString: true }) : encodeToon(input);
|
|
169
|
+
const compressed = stableStringifyInput(compressedInput);
|
|
170
|
+
return buildResult(input, compressedInput, "toon", original, compressed, [
|
|
171
|
+
"toon",
|
|
172
|
+
"json-minify"
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
async function compressPromptMessages(options) {
|
|
176
|
+
try {
|
|
177
|
+
return await compressMessagesWithUsageTap(options);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (options.failOpen === false) {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
return createPromptCompressionFallback(
|
|
183
|
+
options.input,
|
|
184
|
+
options.provider ?? "usagetap",
|
|
185
|
+
error
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function compressWithTheTokenCompany(options) {
|
|
190
|
+
if (!options.tokenCompanyApiKey) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"tokenCompanyApiKey is required when provider is thetokencompany"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
|
|
196
|
+
if (typeof fetchCandidate !== "function") {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"A fetch implementation is required for The Token Company compression"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return compressWithCompatibleRemoteProvider({
|
|
202
|
+
options,
|
|
203
|
+
provider: "thetokencompany",
|
|
204
|
+
endpoint: options.tokenCompanyEndpoint ?? DEFAULT_TTC_ENDPOINT,
|
|
205
|
+
model: options.model ?? options.tokenCompanyModel ?? DEFAULT_TTC_MODEL,
|
|
206
|
+
aggressiveness: options.aggressiveness ?? options.tokenCompanyAggressiveness ?? DEFAULT_TTC_AGGRESSIVENESS,
|
|
207
|
+
apiKey: options.tokenCompanyApiKey,
|
|
208
|
+
appId: options.tokenCompanyAppId,
|
|
209
|
+
providerLabel: "The Token Company"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function compressWithUsageTap(options) {
|
|
213
|
+
const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
|
|
214
|
+
if (typeof fetchCandidate !== "function") {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"A fetch implementation is required for UsageTap prompt compression"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return compressWithCompatibleRemoteProvider({
|
|
220
|
+
options,
|
|
221
|
+
provider: "usagetap",
|
|
222
|
+
endpoint: options.usageTapCompressionEndpoint ?? DEFAULT_USAGETAP_COMPRESSION_ENDPOINT,
|
|
223
|
+
model: options.model ?? options.usageTapCompressionModel ?? options.tokenCompanyModel ?? DEFAULT_TTC_MODEL,
|
|
224
|
+
aggressiveness: options.aggressiveness ?? options.usageTapCompressionAggressiveness ?? options.tokenCompanyAggressiveness ?? DEFAULT_TTC_AGGRESSIVENESS,
|
|
225
|
+
apiKey: options.usageTapCompressionApiKey,
|
|
226
|
+
appId: options.tokenCompanyAppId,
|
|
227
|
+
providerLabel: "UsageTap prompt compression"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function compressMessagesWithUsageTap(options) {
|
|
231
|
+
const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
|
|
232
|
+
if (typeof fetchCandidate !== "function") {
|
|
233
|
+
throw new Error(
|
|
234
|
+
"A fetch implementation is required for UsageTap prompt message compression"
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const aggressiveness = options.aggressiveness ?? options.usageTapCompressionAggressiveness ?? DEFAULT_TTC_AGGRESSIVENESS;
|
|
238
|
+
validateAggressiveness(
|
|
239
|
+
aggressiveness,
|
|
240
|
+
"UsageTap prompt message compression"
|
|
241
|
+
);
|
|
242
|
+
if (options.latencyBudgetMs !== void 0 && (!Number.isFinite(options.latencyBudgetMs) || options.latencyBudgetMs < 0)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"UsageTap prompt message compression latencyBudgetMs must be a non-negative number"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const original = stableStringifyInput(options.input);
|
|
248
|
+
const headers = {
|
|
249
|
+
"content-type": "application/json"
|
|
250
|
+
};
|
|
251
|
+
if (options.usageTapCompressionApiKey) {
|
|
252
|
+
headers.authorization = `Bearer ${options.usageTapCompressionApiKey}`;
|
|
253
|
+
}
|
|
254
|
+
const response = await fetchCandidate(
|
|
255
|
+
options.usageTapCompressionMessagesEndpoint ?? DEFAULT_USAGETAP_MESSAGES_COMPRESSION_ENDPOINT,
|
|
256
|
+
{
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers,
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
...cloneInputRecord(options.input),
|
|
261
|
+
compression_settings: {
|
|
262
|
+
aggressiveness,
|
|
263
|
+
...options.mode === void 0 ? {} : { mode: options.mode },
|
|
264
|
+
...options.latencyBudgetMs === void 0 ? {} : { latency_budget_ms: options.latencyBudgetMs },
|
|
265
|
+
...options.compactEmptyUserMessages === void 0 ? {} : { compact_empty_user_messages: options.compactEmptyUserMessages },
|
|
266
|
+
...options.compactDuplicateUserTextParts === void 0 ? {} : {
|
|
267
|
+
compact_duplicate_user_text_parts: options.compactDuplicateUserTextParts
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}),
|
|
271
|
+
signal: options.signal
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`UsageTap prompt message compression failed with HTTP ${response.status}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const payload = await response.json();
|
|
280
|
+
const compressedInput = payload.compressed_request ?? payload.compressedInput ?? payload.compressed ?? (payload.messages !== void 0 ? { ...cloneInputRecord(options.input), messages: payload.messages } : void 0);
|
|
281
|
+
if (compressedInput === void 0) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
"UsageTap prompt message compression response did not include compressed content"
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const compressed = stableStringifyInput(compressedInput);
|
|
287
|
+
const tokenCounts = normalizeCompatibleTokenCounts(payload);
|
|
288
|
+
return buildResult(
|
|
289
|
+
options.input,
|
|
290
|
+
compressedInput,
|
|
291
|
+
"usagetap",
|
|
292
|
+
original,
|
|
293
|
+
compressed,
|
|
294
|
+
["usagetap", "messages-endpoint"],
|
|
295
|
+
tokenCounts
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
async function compressWithCompatibleRemoteProvider(args) {
|
|
299
|
+
const { options, provider, endpoint, model, aggressiveness, apiKey, appId, providerLabel } = args;
|
|
300
|
+
const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
|
|
301
|
+
const sourceInput = resolvePromptCompressionInput(options);
|
|
302
|
+
const original = stableStringifyInput(sourceInput);
|
|
303
|
+
const heuristic = compressPromptHeuristic(sourceInput);
|
|
304
|
+
const input = typeof heuristic.compressedInput === "string" ? heuristic.compressedInput : stableStringifyInput(heuristic.compressedInput);
|
|
305
|
+
if (!isValidAggressiveness(aggressiveness)) {
|
|
306
|
+
throw new Error(`${providerLabel} aggressiveness must be between 0.0 and 1.0`);
|
|
307
|
+
}
|
|
308
|
+
const headers = {
|
|
309
|
+
"content-type": "application/json"
|
|
310
|
+
};
|
|
311
|
+
if (apiKey) {
|
|
312
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
313
|
+
}
|
|
314
|
+
const response = await fetchCandidate(
|
|
315
|
+
endpoint,
|
|
316
|
+
{
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers,
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
model,
|
|
321
|
+
input,
|
|
322
|
+
...provider === "usagetap" ? { text: input } : {},
|
|
323
|
+
compression_settings: { aggressiveness },
|
|
324
|
+
...appId ? { app_id: appId } : {}
|
|
325
|
+
}),
|
|
326
|
+
signal: options.signal
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`${providerLabel} failed with HTTP ${response.status}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const payload = await response.json();
|
|
335
|
+
const tokenCompanyResult = normalizeTheTokenCompanyCompressResponse(payload);
|
|
336
|
+
const compressedInput = payload.compressedInput ?? payload.compressed ?? tokenCompanyResult?.output ?? payload.output ?? payload.text;
|
|
337
|
+
if (compressedInput === void 0) {
|
|
338
|
+
throw new Error(`${providerLabel} response did not include compressed content`);
|
|
339
|
+
}
|
|
340
|
+
const compressed = stableStringifyInput(compressedInput);
|
|
341
|
+
const tokenCounts = tokenCompanyResult ? {
|
|
342
|
+
originalTokens: tokenCompanyResult.input_tokens,
|
|
343
|
+
compressedTokens: tokenCompanyResult.output_tokens,
|
|
344
|
+
savedTokens: tokenCompanyResult.tokens_saved
|
|
345
|
+
} : void 0;
|
|
346
|
+
return buildResult(
|
|
347
|
+
sourceInput,
|
|
348
|
+
compressedInput,
|
|
349
|
+
provider,
|
|
350
|
+
original,
|
|
351
|
+
compressed,
|
|
352
|
+
[...heuristic.techniques, provider],
|
|
353
|
+
tokenCounts
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
function resolvePromptCompressionInput(options) {
|
|
357
|
+
if (options.input !== void 0) {
|
|
358
|
+
return options.input;
|
|
359
|
+
}
|
|
360
|
+
if (options.text !== void 0) {
|
|
361
|
+
return options.text;
|
|
362
|
+
}
|
|
363
|
+
throw new Error("Prompt compression requires input or text");
|
|
364
|
+
}
|
|
365
|
+
function validateAggressiveness(value, label) {
|
|
366
|
+
if (typeof value === "number") {
|
|
367
|
+
if (!isValidAggressiveness(value)) {
|
|
368
|
+
throw new Error(`${label} aggressiveness must be between 0.0 and 1.0`);
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
for (const aggressiveness of Object.values(value)) {
|
|
373
|
+
if (aggressiveness !== void 0 && !isValidAggressiveness(aggressiveness)) {
|
|
374
|
+
throw new Error(`${label} aggressiveness must be between 0.0 and 1.0`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function isValidAggressiveness(value) {
|
|
379
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
380
|
+
}
|
|
381
|
+
function cloneInputRecord(input) {
|
|
382
|
+
return input && typeof input === "object" && !Array.isArray(input) ? { ...input } : { input };
|
|
383
|
+
}
|
|
384
|
+
function normalizeCompatibleTokenCounts(data) {
|
|
385
|
+
const inputTokens = typeof data.input_tokens === "number" ? data.input_tokens : data.original_input_tokens;
|
|
386
|
+
const outputTokens = data.output_tokens;
|
|
387
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number") {
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
originalTokens: inputTokens,
|
|
392
|
+
compressedTokens: outputTokens,
|
|
393
|
+
savedTokens: typeof data.tokens_saved === "number" ? data.tokens_saved : inputTokens - outputTokens
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function normalizeTheTokenCompanyCompressResponse(data) {
|
|
397
|
+
if (typeof data.output !== "string" || typeof data.output_tokens !== "number") {
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
const inputTokens = typeof data.input_tokens === "number" ? data.input_tokens : data.original_input_tokens;
|
|
401
|
+
if (typeof inputTokens !== "number") {
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
const tokensSaved = typeof data.tokens_saved === "number" ? data.tokens_saved : inputTokens - data.output_tokens;
|
|
405
|
+
const compressionRatio = typeof data.compression_ratio === "number" ? data.compression_ratio : data.output_tokens === 0 ? 0 : inputTokens / data.output_tokens;
|
|
406
|
+
return {
|
|
407
|
+
output: data.output,
|
|
408
|
+
output_tokens: data.output_tokens,
|
|
409
|
+
input_tokens: inputTokens,
|
|
410
|
+
tokens_saved: tokensSaved,
|
|
411
|
+
compression_ratio: compressionRatio
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function createPromptCompressionFallback(input, provider = "heuristic", error) {
|
|
415
|
+
const original = stableStringifyInput(input);
|
|
416
|
+
const techniques = ["fallback-original"];
|
|
417
|
+
if (error) {
|
|
418
|
+
techniques.push("compression-error");
|
|
419
|
+
}
|
|
420
|
+
return buildResult(input, input, provider, original, original, techniques);
|
|
421
|
+
}
|
|
422
|
+
function buildResult(input, compressedInput, provider, original, compressed, techniques, tokenCounts) {
|
|
423
|
+
const originalCharacters = original.length;
|
|
424
|
+
const compressedCharacters = compressed.length;
|
|
425
|
+
const savedCharacters = Math.max(
|
|
426
|
+
0,
|
|
427
|
+
originalCharacters - compressedCharacters
|
|
428
|
+
);
|
|
429
|
+
const originalTokens = tokenCounts?.originalTokens ?? estimatePromptTokens(original);
|
|
430
|
+
const compressedTokens = tokenCounts?.compressedTokens ?? estimatePromptTokens(compressed);
|
|
431
|
+
const savedTokens = Math.max(
|
|
432
|
+
0,
|
|
433
|
+
tokenCounts?.savedTokens ?? originalTokens - compressedTokens
|
|
434
|
+
);
|
|
435
|
+
return {
|
|
436
|
+
input,
|
|
437
|
+
compressedInput,
|
|
438
|
+
provider,
|
|
439
|
+
originalCharacters,
|
|
440
|
+
compressedCharacters,
|
|
441
|
+
savedCharacters,
|
|
442
|
+
originalTokens,
|
|
443
|
+
compressedTokens,
|
|
444
|
+
savedTokens,
|
|
445
|
+
tokenSavingsRatio: originalTokens > 0 ? savedTokens / originalTokens : 0,
|
|
446
|
+
savingsRatio: originalCharacters > 0 ? savedCharacters / originalCharacters : 0,
|
|
447
|
+
techniques
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function estimatePromptTokens(input) {
|
|
451
|
+
const text = typeof input === "string" ? input : stableStringifyInput(input);
|
|
452
|
+
return text.match(/[\p{L}\p{N}]+|[^\s]/gu)?.length ?? 0;
|
|
453
|
+
}
|
|
454
|
+
function compressValue(value, techniques, options) {
|
|
455
|
+
if (typeof value === "string") return compressText(value, techniques, options);
|
|
456
|
+
if (Array.isArray(value)) {
|
|
457
|
+
techniques.add("json-minify");
|
|
458
|
+
return value.map((item) => compressValue(item, techniques, options));
|
|
459
|
+
}
|
|
460
|
+
if (value && typeof value === "object") {
|
|
461
|
+
techniques.add("json-minify");
|
|
462
|
+
return Object.keys(value).reduce((acc, key) => {
|
|
463
|
+
const child = value[key];
|
|
464
|
+
if (child !== void 0) {
|
|
465
|
+
acc[key] = compressValue(child, techniques, options);
|
|
466
|
+
}
|
|
467
|
+
return acc;
|
|
468
|
+
}, {});
|
|
469
|
+
}
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
function compressText(value, techniques, options) {
|
|
473
|
+
const protectedSpans = [];
|
|
474
|
+
const text = value.replace(PROTECTED_TEXT_PATTERN, (match) => {
|
|
475
|
+
const placeholder = `__USAGETAP_PROTECTED_${protectedSpans.length}__`;
|
|
476
|
+
protectedSpans.push(match);
|
|
477
|
+
techniques.add("protected-text");
|
|
478
|
+
return placeholder;
|
|
479
|
+
});
|
|
480
|
+
const compressed = compressTextWithoutProtection(text, techniques, options);
|
|
481
|
+
return protectedSpans.reduce(
|
|
482
|
+
(output, span, index) => output.replace(`__USAGETAP_PROTECTED_${index}__`, span),
|
|
483
|
+
compressed
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
function compressTextWithoutProtection(value, techniques, options) {
|
|
487
|
+
const fencePattern = /```([\w-]+)?\n([\s\S]*?)```/g;
|
|
488
|
+
const parts = [];
|
|
489
|
+
let cursor = 0;
|
|
490
|
+
let match;
|
|
491
|
+
while ((match = fencePattern.exec(value)) !== null) {
|
|
492
|
+
const before = value.slice(cursor, match.index);
|
|
493
|
+
const compressedBefore = compressPlainTextAndEmbeddedJson(
|
|
494
|
+
before,
|
|
495
|
+
techniques,
|
|
496
|
+
options
|
|
497
|
+
);
|
|
498
|
+
if (compressedBefore) parts.push(compressedBefore);
|
|
499
|
+
const lang = match[1]?.toLowerCase();
|
|
500
|
+
const code = cleanCodeBlock(match[2] ?? "");
|
|
501
|
+
const compressedCode = lang === "json" ? compressJsonText(code, techniques, options) : void 0;
|
|
502
|
+
if (compressedCode?.format === "toon") {
|
|
503
|
+
parts.push(`\`\`\`toon
|
|
504
|
+
${compressedCode.text}
|
|
505
|
+
\`\`\``);
|
|
506
|
+
} else if (compressedCode?.format === "json") {
|
|
507
|
+
parts.push(`\`\`\`json
|
|
508
|
+
${compressedCode.text}
|
|
509
|
+
\`\`\``);
|
|
510
|
+
} else {
|
|
511
|
+
if (code !== match[2]) {
|
|
512
|
+
techniques.add("code-whitespace");
|
|
513
|
+
}
|
|
514
|
+
parts.push(lang ? `\`\`\`${lang}
|
|
515
|
+
${code}
|
|
516
|
+
\`\`\`` : `\`\`\`
|
|
517
|
+
${code}
|
|
518
|
+
\`\`\``);
|
|
519
|
+
}
|
|
520
|
+
cursor = match.index + match[0].length;
|
|
521
|
+
}
|
|
522
|
+
const after = compressPlainTextAndEmbeddedJson(value.slice(cursor), techniques, options);
|
|
523
|
+
if (after) parts.push(after);
|
|
524
|
+
return parts.join("\n").trim();
|
|
525
|
+
}
|
|
526
|
+
function compressPlainText(value, techniques) {
|
|
527
|
+
const compressed = value.split("\n").map((line) => line.trim()).filter((line) => line).join("\n").replace(/[ \t]{2,}/g, " ").trim();
|
|
528
|
+
if (compressed !== value.trim()) {
|
|
529
|
+
techniques.add("text-whitespace");
|
|
530
|
+
}
|
|
531
|
+
return compressed;
|
|
532
|
+
}
|
|
533
|
+
function compressPlainTextAndEmbeddedJson(value, techniques, options) {
|
|
534
|
+
const normalized = compressPlainText(value, techniques);
|
|
535
|
+
return compressEmbeddedJson(normalized, techniques, options);
|
|
536
|
+
}
|
|
537
|
+
function cleanCodeBlock(code) {
|
|
538
|
+
const lines = code.replace(/\r\n/g, "\n").split("\n");
|
|
539
|
+
while (lines.length && lines[0].trim() === "") lines.shift();
|
|
540
|
+
while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
|
|
541
|
+
const commonIndent = lines.filter((line) => line.trim()).reduce((min, line) => {
|
|
542
|
+
const indent = /^[ \t]*/.exec(line)?.[0].length ?? 0;
|
|
543
|
+
return min === void 0 ? indent : Math.min(min, indent);
|
|
544
|
+
}, void 0);
|
|
545
|
+
return lines.map((line) => commonIndent ? line.slice(commonIndent) : line).join("\n").replace(/[ \t]+$/gm, "");
|
|
546
|
+
}
|
|
547
|
+
function stableStringifyInput(input) {
|
|
548
|
+
if (typeof input === "string") return input;
|
|
549
|
+
return JSON.stringify(input) ?? String(input);
|
|
550
|
+
}
|
|
551
|
+
function compressJsonText(text, techniques, options) {
|
|
552
|
+
const parsed = safeParseJson(text);
|
|
553
|
+
if (parsed === void 0) {
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
const compactJson = JSON.stringify(parsed);
|
|
557
|
+
const candidates = [
|
|
558
|
+
{ format: "json", text: compactJson }
|
|
559
|
+
];
|
|
560
|
+
if (options.allowToonString || shouldUseToonForJson(parsed)) {
|
|
561
|
+
candidates.push({ format: "toon", text: encodeToon(parsed) });
|
|
562
|
+
}
|
|
563
|
+
const originalLength = text.trim().length;
|
|
564
|
+
const best = candidates.reduce(
|
|
565
|
+
(winner, candidate) => candidate.text.length < winner.text.length ? candidate : winner
|
|
566
|
+
);
|
|
567
|
+
if (best.text.length >= originalLength) {
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
techniques.add(best.format === "toon" ? "embedded-json-toon" : "embedded-json-minify");
|
|
571
|
+
return best;
|
|
572
|
+
}
|
|
573
|
+
function compressEmbeddedJson(text, techniques, options) {
|
|
574
|
+
let result = "";
|
|
575
|
+
let cursor = 0;
|
|
576
|
+
while (cursor < text.length) {
|
|
577
|
+
const start = findNextJsonStart(text, cursor);
|
|
578
|
+
if (start < 0) {
|
|
579
|
+
result += text.slice(cursor);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
result += text.slice(cursor, start);
|
|
583
|
+
const span = findBalancedJsonSpan(text, start);
|
|
584
|
+
if (!span) {
|
|
585
|
+
result += text[start];
|
|
586
|
+
cursor = start + 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const candidate = compressJsonText(span.text, techniques, options);
|
|
590
|
+
if (candidate) {
|
|
591
|
+
result += candidate.text;
|
|
592
|
+
} else {
|
|
593
|
+
result += span.text;
|
|
594
|
+
}
|
|
595
|
+
cursor = span.end;
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
function findNextJsonStart(text, from) {
|
|
600
|
+
const objectStart = text.indexOf("{", from);
|
|
601
|
+
const arrayStart = text.indexOf("[", from);
|
|
602
|
+
if (objectStart < 0) return arrayStart;
|
|
603
|
+
if (arrayStart < 0) return objectStart;
|
|
604
|
+
return Math.min(objectStart, arrayStart);
|
|
605
|
+
}
|
|
606
|
+
function findBalancedJsonSpan(text, start) {
|
|
607
|
+
const opener = text[start];
|
|
608
|
+
const closer = opener === "{" ? "}" : opener === "[" ? "]" : void 0;
|
|
609
|
+
if (!closer) return void 0;
|
|
610
|
+
const stack = [closer];
|
|
611
|
+
let inString = false;
|
|
612
|
+
let escaped = false;
|
|
613
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
614
|
+
const char = text[index];
|
|
615
|
+
if (inString) {
|
|
616
|
+
if (escaped) {
|
|
617
|
+
escaped = false;
|
|
618
|
+
} else if (char === "\\") {
|
|
619
|
+
escaped = true;
|
|
620
|
+
} else if (char === '"') {
|
|
621
|
+
inString = false;
|
|
622
|
+
}
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (char === '"') {
|
|
626
|
+
inString = true;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (char === "{" || char === "[") {
|
|
630
|
+
stack.push(char === "{" ? "}" : "]");
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (char === stack[stack.length - 1]) {
|
|
634
|
+
stack.pop();
|
|
635
|
+
if (!stack.length) {
|
|
636
|
+
const end = index + 1;
|
|
637
|
+
return { text: text.slice(start, end), end };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return void 0;
|
|
642
|
+
}
|
|
643
|
+
function safeParseJson(text) {
|
|
644
|
+
try {
|
|
645
|
+
return JSON.parse(text);
|
|
646
|
+
} catch {
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function shouldUseToonForJson(value) {
|
|
651
|
+
if (Array.isArray(value)) {
|
|
652
|
+
return isUniformObjectArray(value) || value.some(shouldUseToonForJson);
|
|
653
|
+
}
|
|
654
|
+
if (isPlainObject(value)) {
|
|
655
|
+
return Object.values(value).some(shouldUseToonForJson);
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
function encodeToon(value, indent = 0) {
|
|
660
|
+
if (isPrimitive(value)) {
|
|
661
|
+
return scalarToToon(value);
|
|
662
|
+
}
|
|
663
|
+
if (Array.isArray(value)) {
|
|
664
|
+
return encodeArrayToon(value, indent);
|
|
665
|
+
}
|
|
666
|
+
if (isPlainObject(value)) {
|
|
667
|
+
const lines = [];
|
|
668
|
+
for (const [key, child] of Object.entries(value)) {
|
|
669
|
+
lines.push(...encodePropertyToon(key, child, indent));
|
|
670
|
+
}
|
|
671
|
+
return lines.join("\n");
|
|
672
|
+
}
|
|
673
|
+
return scalarToToon(String(value));
|
|
674
|
+
}
|
|
675
|
+
function encodePropertyToon(key, value, indent) {
|
|
676
|
+
const prefix = " ".repeat(indent);
|
|
677
|
+
const toonKey = keyToToon(key);
|
|
678
|
+
if (isPrimitive(value)) {
|
|
679
|
+
return [`${prefix}${toonKey}: ${scalarToToon(value)}`];
|
|
680
|
+
}
|
|
681
|
+
if (Array.isArray(value)) {
|
|
682
|
+
if (value.every(isPrimitive)) {
|
|
683
|
+
return [`${prefix}${toonKey}[${value.length}]: ${value.map(scalarToToon).join(",")}`];
|
|
684
|
+
}
|
|
685
|
+
if (isUniformObjectArray(value)) {
|
|
686
|
+
const fields = Object.keys(value[0]);
|
|
687
|
+
const header = `${prefix}${toonKey}[${value.length}]{${fields.map(keyToToon).join(",")}}:`;
|
|
688
|
+
const rows = value.map(
|
|
689
|
+
(item) => `${" ".repeat(indent + 2)}${fields.map(
|
|
690
|
+
(field) => scalarToToon(item[field])
|
|
691
|
+
).join(",")}`
|
|
692
|
+
);
|
|
693
|
+
return [header, ...rows];
|
|
694
|
+
}
|
|
695
|
+
return [
|
|
696
|
+
`${prefix}${toonKey}[${value.length}]:`,
|
|
697
|
+
...value.flatMap((item, index) => {
|
|
698
|
+
if (isPrimitive(item)) {
|
|
699
|
+
return [`${" ".repeat(indent + 2)}- ${scalarToToon(item)}`];
|
|
700
|
+
}
|
|
701
|
+
return [
|
|
702
|
+
`${" ".repeat(indent + 2)}- item${index}:`,
|
|
703
|
+
...encodeToon(item, indent + 4).split("\n")
|
|
704
|
+
];
|
|
705
|
+
})
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
return [`${prefix}${toonKey}:`, ...encodeToon(value, indent + 2).split("\n")];
|
|
709
|
+
}
|
|
710
|
+
function encodeArrayToon(value, indent) {
|
|
711
|
+
if (value.every(isPrimitive)) {
|
|
712
|
+
return `[${value.length}]: ${value.map(scalarToToon).join(",")}`;
|
|
713
|
+
}
|
|
714
|
+
if (isUniformObjectArray(value)) {
|
|
715
|
+
const fields = Object.keys(value[0]);
|
|
716
|
+
return [
|
|
717
|
+
`[${value.length}]{${fields.map(keyToToon).join(",")}}:`,
|
|
718
|
+
...value.map(
|
|
719
|
+
(item) => `${" ".repeat(indent + 2)}${fields.map(
|
|
720
|
+
(field) => scalarToToon(item[field])
|
|
721
|
+
).join(",")}`
|
|
722
|
+
)
|
|
723
|
+
].join("\n");
|
|
724
|
+
}
|
|
725
|
+
return value.flatMap((item, index) => [
|
|
726
|
+
`${" ".repeat(indent)}- item${index}:`,
|
|
727
|
+
...encodeToon(item, indent + 2).split("\n")
|
|
728
|
+
]).join("\n");
|
|
729
|
+
}
|
|
730
|
+
function isUniformObjectArray(value) {
|
|
731
|
+
if (!value.length || !value.every(isPlainObject)) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
const fields = Object.keys(value[0]);
|
|
735
|
+
if (!fields.length) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
return value.every((item) => {
|
|
739
|
+
const record = item;
|
|
740
|
+
const itemFields = Object.keys(record);
|
|
741
|
+
return itemFields.length === fields.length && fields.every((field) => itemFields.includes(field) && isPrimitive(record[field]));
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function isPlainObject(value) {
|
|
745
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
746
|
+
}
|
|
747
|
+
function isPrimitive(value) {
|
|
748
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
749
|
+
}
|
|
750
|
+
function keyToToon(key) {
|
|
751
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
|
|
752
|
+
}
|
|
753
|
+
function scalarToToon(value) {
|
|
754
|
+
if (value === null) return "null";
|
|
755
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
756
|
+
return String(value);
|
|
757
|
+
}
|
|
758
|
+
const text = String(value);
|
|
759
|
+
if (text && !/^(true|false|null|-?\d+(?:\.\d+)?)$/i.test(text) && /^[A-Za-z0-9_./@-]+(?: [A-Za-z0-9_./@-]+)*$/.test(text)) {
|
|
760
|
+
return text;
|
|
761
|
+
}
|
|
762
|
+
return JSON.stringify(text);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/resources.ts
|
|
766
|
+
var CANONICAL_MEDIA_TYPE = "application/vnd.usagetap.v1+json";
|
|
767
|
+
var DEFAULT_GATEWAY_BASE_URL = "https://gateway.usagetap.com";
|
|
768
|
+
function normalizedBaseUrl(value) {
|
|
769
|
+
return `${value.replace(/\/+$/, "")}/`;
|
|
770
|
+
}
|
|
771
|
+
function errorMessage(payload, status) {
|
|
772
|
+
if (payload && typeof payload === "object") {
|
|
773
|
+
const record = payload;
|
|
774
|
+
const error = record.error;
|
|
775
|
+
if (error && typeof error === "object") {
|
|
776
|
+
const message2 = error.message;
|
|
777
|
+
if (typeof message2 === "string" && message2) return message2;
|
|
778
|
+
}
|
|
779
|
+
const message = record.message;
|
|
780
|
+
if (typeof message === "string" && message) return message;
|
|
781
|
+
}
|
|
782
|
+
return `UsageTap request failed with HTTP ${status}`;
|
|
783
|
+
}
|
|
784
|
+
function errorCode(status) {
|
|
785
|
+
if (status === 401 || status === 403) return "USAGETAP_AUTH_ERROR";
|
|
786
|
+
if (status === 429) return "USAGETAP_RATE_LIMITED";
|
|
787
|
+
if (status >= 500) return "USAGETAP_SERVER_ERROR";
|
|
788
|
+
return "USAGETAP_BAD_REQUEST";
|
|
789
|
+
}
|
|
790
|
+
var ResourceTransport = class {
|
|
791
|
+
baseUrl;
|
|
792
|
+
apiKey;
|
|
793
|
+
fetchImpl;
|
|
794
|
+
defaultHeaders;
|
|
795
|
+
sdkVersion;
|
|
796
|
+
constructor(baseUrl, config) {
|
|
797
|
+
this.baseUrl = normalizedBaseUrl(baseUrl);
|
|
798
|
+
this.apiKey = config.apiKey;
|
|
799
|
+
this.fetchImpl = config.fetchImpl;
|
|
800
|
+
this.defaultHeaders = config.headers ?? {};
|
|
801
|
+
this.sdkVersion = config.sdkVersion;
|
|
802
|
+
}
|
|
803
|
+
async request(request) {
|
|
804
|
+
const body = request.body === void 0 ? void 0 : JSON.stringify(request.body);
|
|
805
|
+
const headers = {
|
|
806
|
+
...this.defaultHeaders,
|
|
807
|
+
accept: request.response === "data" ? CANONICAL_MEDIA_TYPE : request.response === "ndjson" ? "application/x-ndjson" : "application/json",
|
|
808
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
809
|
+
"x-usage-sdk": `js/${this.sdkVersion}`,
|
|
810
|
+
...body ? { "content-type": "application/json" } : {},
|
|
811
|
+
...request.options?.idempotencyKey ? { "idempotency-key": request.options.idempotencyKey } : {},
|
|
812
|
+
...request.options?.headers
|
|
813
|
+
};
|
|
814
|
+
let response;
|
|
815
|
+
try {
|
|
816
|
+
response = await this.fetchImpl(
|
|
817
|
+
new URL(request.path.replace(/^\/+/, ""), this.baseUrl),
|
|
818
|
+
{
|
|
819
|
+
method: request.method,
|
|
820
|
+
headers,
|
|
821
|
+
body,
|
|
822
|
+
signal: request.options?.signal
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
throw new UsageTapError(
|
|
827
|
+
"USAGETAP_NETWORK_ERROR",
|
|
828
|
+
"Failed to reach UsageTap",
|
|
829
|
+
{ retryable: true, cause: error }
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-usage-correlation-id") ?? void 0;
|
|
833
|
+
const text = await response.text();
|
|
834
|
+
let payload;
|
|
835
|
+
if (text && request.response !== "ndjson") {
|
|
836
|
+
try {
|
|
837
|
+
payload = JSON.parse(text);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
throw new UsageTapError(
|
|
840
|
+
"USAGETAP_INVALID_RESPONSE",
|
|
841
|
+
"UsageTap returned invalid JSON",
|
|
842
|
+
{ status: response.status, correlationId: requestId, cause: error }
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
throw new UsageTapError(
|
|
848
|
+
errorCode(response.status),
|
|
849
|
+
errorMessage(payload, response.status),
|
|
850
|
+
{
|
|
851
|
+
status: response.status,
|
|
852
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
853
|
+
correlationId: requestId,
|
|
854
|
+
details: payload && typeof payload === "object" ? payload : void 0
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (request.response === "ndjson") {
|
|
859
|
+
if (!text.trim()) return [];
|
|
860
|
+
try {
|
|
861
|
+
return text.trim().split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
862
|
+
} catch (error) {
|
|
863
|
+
throw new UsageTapError(
|
|
864
|
+
"USAGETAP_INVALID_RESPONSE",
|
|
865
|
+
"UsageTap returned invalid NDJSON",
|
|
866
|
+
{ status: response.status, correlationId: requestId, cause: error }
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (request.response === "data") {
|
|
871
|
+
if (!payload || typeof payload !== "object" || !("data" in payload)) {
|
|
872
|
+
throw new UsageTapError(
|
|
873
|
+
"USAGETAP_INVALID_RESPONSE",
|
|
874
|
+
"UsageTap response missing data",
|
|
875
|
+
{ status: response.status, correlationId: requestId }
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return payload.data;
|
|
879
|
+
}
|
|
880
|
+
if (payload === void 0) {
|
|
881
|
+
throw new UsageTapError(
|
|
882
|
+
"USAGETAP_INVALID_RESPONSE",
|
|
883
|
+
"UsageTap response was empty",
|
|
884
|
+
{ status: response.status, correlationId: requestId }
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return payload;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
function resourceId(value, keys, label) {
|
|
891
|
+
const id = typeof value === "string" ? value : keys.map((key) => value[key]).find((candidate) => Boolean(candidate?.trim()));
|
|
892
|
+
if (!id?.trim()) {
|
|
893
|
+
throw new UsageTapError(
|
|
894
|
+
"USAGETAP_BAD_REQUEST",
|
|
895
|
+
`${label} requires a non-empty ID`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return id.trim();
|
|
899
|
+
}
|
|
900
|
+
function terminalSummary(status) {
|
|
901
|
+
return status === "COMPLETE" || status === "FAILED";
|
|
902
|
+
}
|
|
903
|
+
function terminalGatewayBatch(status) {
|
|
904
|
+
return ["completed", "failed", "expired", "cancelled"].includes(status);
|
|
905
|
+
}
|
|
906
|
+
function validateWaitOptions(options) {
|
|
907
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
908
|
+
const timeoutMs = options.timeoutMs ?? 3 * 6e4;
|
|
909
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 0) {
|
|
910
|
+
throw new UsageTapError(
|
|
911
|
+
"USAGETAP_BAD_REQUEST",
|
|
912
|
+
"pollIntervalMs must be a non-negative number"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
|
|
916
|
+
throw new UsageTapError(
|
|
917
|
+
"USAGETAP_BAD_REQUEST",
|
|
918
|
+
"timeoutMs must be a positive number"
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return { pollIntervalMs, timeoutMs };
|
|
922
|
+
}
|
|
923
|
+
var SummarizationResource = class {
|
|
924
|
+
summaries;
|
|
925
|
+
batches;
|
|
926
|
+
profiles;
|
|
927
|
+
measurements;
|
|
928
|
+
transport;
|
|
929
|
+
constructor(config) {
|
|
930
|
+
this.transport = new ResourceTransport(config.apiBaseUrl, config);
|
|
931
|
+
this.summaries = {
|
|
932
|
+
create: (params, options) => this.transport.request({
|
|
933
|
+
method: "POST",
|
|
934
|
+
path: "/v1/compression/summaries",
|
|
935
|
+
body: params,
|
|
936
|
+
options,
|
|
937
|
+
response: "data"
|
|
938
|
+
}),
|
|
939
|
+
retrieve: (jobId, options) => this.transport.request({
|
|
940
|
+
method: "GET",
|
|
941
|
+
path: `/v1/compression/jobs/${encodeURIComponent(
|
|
942
|
+
resourceId(jobId, ["jobId"], "summaries.retrieve")
|
|
943
|
+
)}`,
|
|
944
|
+
options,
|
|
945
|
+
response: "data"
|
|
946
|
+
}),
|
|
947
|
+
wait: (job, options) => this.waitForSummary(job, options)
|
|
948
|
+
};
|
|
949
|
+
this.batches = {
|
|
950
|
+
create: (params, options) => this.transport.request({
|
|
951
|
+
method: "POST",
|
|
952
|
+
path: "/v1/compression/batches",
|
|
953
|
+
body: params,
|
|
954
|
+
options,
|
|
955
|
+
response: "data"
|
|
956
|
+
}),
|
|
957
|
+
retrieve: (batchId, options) => this.transport.request({
|
|
958
|
+
method: "GET",
|
|
959
|
+
path: `/v1/compression/batches/${encodeURIComponent(
|
|
960
|
+
resourceId(batchId, ["batchId"], "summarization.batches.retrieve")
|
|
961
|
+
)}`,
|
|
962
|
+
options,
|
|
963
|
+
response: "data"
|
|
964
|
+
}),
|
|
965
|
+
wait: (batch, options) => this.waitForBatch(batch, options)
|
|
966
|
+
};
|
|
967
|
+
this.profiles = {
|
|
968
|
+
retrieve: (profile, options) => this.transport.request({
|
|
969
|
+
method: "GET",
|
|
970
|
+
path: `/v1/compression/profiles/${encodeURIComponent(
|
|
971
|
+
resourceId(profile, [], "summarization.profiles.retrieve")
|
|
972
|
+
)}`,
|
|
973
|
+
options,
|
|
974
|
+
response: "data"
|
|
975
|
+
})
|
|
976
|
+
};
|
|
977
|
+
this.measurements = {
|
|
978
|
+
create: (params, options) => this.transport.request({
|
|
979
|
+
method: "POST",
|
|
980
|
+
path: "/v1/compression/measurements",
|
|
981
|
+
body: params,
|
|
982
|
+
options,
|
|
983
|
+
response: "data"
|
|
984
|
+
})
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
async waitForSummary(value, options = {}) {
|
|
988
|
+
const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
|
|
989
|
+
const deadline = Date.now() + timeoutMs;
|
|
990
|
+
let job = typeof value === "string" ? await this.summaries.retrieve(value, options) : value;
|
|
991
|
+
while (!terminalSummary(job.status)) {
|
|
992
|
+
if (Date.now() >= deadline) {
|
|
993
|
+
throw new UsageTapError(
|
|
994
|
+
"USAGETAP_RETRY_EXHAUSTED",
|
|
995
|
+
`Summarization job ${job.jobId} did not finish before timeout`,
|
|
996
|
+
{ retryable: true }
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
await sleep(pollIntervalMs, options.signal);
|
|
1000
|
+
job = await this.summaries.retrieve(job.jobId, options);
|
|
1001
|
+
}
|
|
1002
|
+
return job;
|
|
1003
|
+
}
|
|
1004
|
+
async waitForBatch(value, options = {}) {
|
|
1005
|
+
const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
|
|
1006
|
+
const deadline = Date.now() + timeoutMs;
|
|
1007
|
+
let batch = typeof value === "string" ? await this.batches.retrieve(value, options) : value;
|
|
1008
|
+
while (!terminalSummary(batch.status)) {
|
|
1009
|
+
if (Date.now() >= deadline) {
|
|
1010
|
+
throw new UsageTapError(
|
|
1011
|
+
"USAGETAP_RETRY_EXHAUSTED",
|
|
1012
|
+
`Summarization batch ${batch.batchId} did not finish before timeout`,
|
|
1013
|
+
{ retryable: true }
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
await sleep(pollIntervalMs, options.signal);
|
|
1017
|
+
batch = await this.batches.retrieve(batch.batchId, options);
|
|
1018
|
+
}
|
|
1019
|
+
return batch;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
var GatewayResource = class {
|
|
1023
|
+
chat;
|
|
1024
|
+
/** Buffered OpenAI Responses-compatible requests. */
|
|
1025
|
+
responses;
|
|
1026
|
+
models;
|
|
1027
|
+
batches;
|
|
1028
|
+
transport;
|
|
1029
|
+
idempotencyGenerator;
|
|
1030
|
+
constructor(config) {
|
|
1031
|
+
this.transport = new ResourceTransport(
|
|
1032
|
+
config.gatewayBaseUrl ?? DEFAULT_GATEWAY_BASE_URL,
|
|
1033
|
+
config
|
|
1034
|
+
);
|
|
1035
|
+
this.idempotencyGenerator = config.idempotencyGenerator ?? createIdempotencyKey;
|
|
1036
|
+
this.chat = {
|
|
1037
|
+
completions: {
|
|
1038
|
+
create: (params, options) => this.transport.request({
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
path: "/v1/chat/completions",
|
|
1041
|
+
body: params,
|
|
1042
|
+
options,
|
|
1043
|
+
response: "json"
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
this.responses = {
|
|
1048
|
+
create: (params, options) => this.transport.request({
|
|
1049
|
+
method: "POST",
|
|
1050
|
+
path: "/v1/responses",
|
|
1051
|
+
body: params,
|
|
1052
|
+
options,
|
|
1053
|
+
response: "json"
|
|
1054
|
+
})
|
|
1055
|
+
};
|
|
1056
|
+
this.models = {
|
|
1057
|
+
list: (options) => this.transport.request({
|
|
1058
|
+
method: "GET",
|
|
1059
|
+
path: "/v1/models",
|
|
1060
|
+
options,
|
|
1061
|
+
response: "json"
|
|
1062
|
+
})
|
|
1063
|
+
};
|
|
1064
|
+
this.batches = {
|
|
1065
|
+
create: (params, options = {}) => this.transport.request({
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
path: "/v1/batches",
|
|
1068
|
+
body: params,
|
|
1069
|
+
options: {
|
|
1070
|
+
...options,
|
|
1071
|
+
idempotencyKey: options.idempotencyKey ?? this.idempotencyGenerator()
|
|
1072
|
+
},
|
|
1073
|
+
response: "json"
|
|
1074
|
+
}),
|
|
1075
|
+
retrieve: (batchId, options) => this.transport.request({
|
|
1076
|
+
method: "GET",
|
|
1077
|
+
path: `/v1/batches/${encodeURIComponent(
|
|
1078
|
+
resourceId(batchId, ["id"], "gateway.batches.retrieve")
|
|
1079
|
+
)}`,
|
|
1080
|
+
options,
|
|
1081
|
+
response: "json"
|
|
1082
|
+
}),
|
|
1083
|
+
wait: (batch, options) => this.waitForBatch(batch, options),
|
|
1084
|
+
cancel: (batchId, options) => this.transport.request({
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
path: `/v1/batches/${encodeURIComponent(
|
|
1087
|
+
resourceId(batchId, ["id"], "gateway.batches.cancel")
|
|
1088
|
+
)}/cancel`,
|
|
1089
|
+
options,
|
|
1090
|
+
response: "json"
|
|
1091
|
+
}),
|
|
1092
|
+
results: (batchId, options) => this.transport.request({
|
|
1093
|
+
method: "GET",
|
|
1094
|
+
path: `/v1/batches/${encodeURIComponent(
|
|
1095
|
+
resourceId(batchId, ["id"], "gateway.batches.results")
|
|
1096
|
+
)}/results`,
|
|
1097
|
+
options,
|
|
1098
|
+
response: "ndjson"
|
|
1099
|
+
})
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
async waitForBatch(value, options = {}) {
|
|
1103
|
+
const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
|
|
1104
|
+
const deadline = Date.now() + timeoutMs;
|
|
1105
|
+
let batch = typeof value === "string" ? await this.batches.retrieve(value, options) : value;
|
|
1106
|
+
while (!terminalGatewayBatch(batch.status)) {
|
|
1107
|
+
if (Date.now() >= deadline) {
|
|
1108
|
+
throw new UsageTapError(
|
|
1109
|
+
"USAGETAP_RETRY_EXHAUSTED",
|
|
1110
|
+
`Gateway batch ${batch.id} did not finish before timeout`,
|
|
1111
|
+
{ retryable: true }
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
await sleep(pollIntervalMs, options.signal);
|
|
1115
|
+
batch = await this.batches.retrieve(batch.id, options);
|
|
1116
|
+
}
|
|
1117
|
+
return batch;
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// src/client.ts
|
|
1122
|
+
var CALL_BEGIN_PATH = "call_begin";
|
|
1123
|
+
var CALL_END_PATH = "call_end";
|
|
1124
|
+
var COMPRESS_PROMPT_PATH = "compress_prompt";
|
|
1125
|
+
var SAMPLES_PATH = "samples";
|
|
1126
|
+
var SAMPLING_SETTINGS_PATH = "sampling/settings";
|
|
1127
|
+
var SAMPLING_DECIDE_PATH = "sampling/decide";
|
|
1128
|
+
var CHECK_USAGE_PATH = "customers/{customerId}/usage";
|
|
1129
|
+
var CREATE_CUSTOMER_PATH = "customers";
|
|
1130
|
+
var CHANGE_PLAN_PATH = "customers/{customerId}/change_plan";
|
|
1131
|
+
var INCREMENT_CUSTOM_METER_PATH = "custom_meter";
|
|
1132
|
+
var AUTH_HEADER = "authorization";
|
|
1133
|
+
var API_KEY_HEADER = "x-api-key";
|
|
1134
|
+
var CORRELATION_HEADER = "x-usage-correlation-id";
|
|
1135
|
+
var IDEMPOTENCY_HEADER = "idempotency-key";
|
|
1136
|
+
var SDK_HEADER = "x-usage-sdk";
|
|
1137
|
+
var USER_AGENT = "UsageTapClient";
|
|
1138
|
+
var CANONICAL_MEDIA_TYPE2 = "application/vnd.usagetap.v1+json";
|
|
1139
|
+
var DEFAULT_BASE_URL = "https://api.usagetap.com";
|
|
1140
|
+
var DEFAULT_RUN_INACTIVITY_MS = 60 * 60 * 1e3;
|
|
1141
|
+
var SDK_VERSION = "1.7.0" ;
|
|
1142
|
+
var HAS_WINDOW = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
|
|
1143
|
+
var UsageTapClient = class {
|
|
1144
|
+
/** OpenAI-compatible chat, model, and native batch operations. */
|
|
1145
|
+
gateway;
|
|
1146
|
+
/** Published-profile context summarization operations. */
|
|
1147
|
+
summarization;
|
|
1148
|
+
apiKey;
|
|
1149
|
+
baseUrl;
|
|
1150
|
+
fetchImpl;
|
|
1151
|
+
defaultFeature;
|
|
1152
|
+
defaultTags;
|
|
1153
|
+
defaultHeaders;
|
|
1154
|
+
retryDefaults;
|
|
1155
|
+
idempotencyGenerator;
|
|
1156
|
+
logFn;
|
|
1157
|
+
metricFn;
|
|
1158
|
+
authHeader;
|
|
1159
|
+
autoIdempotency;
|
|
1160
|
+
tokenCompanyApiKey;
|
|
1161
|
+
tokenCompanyEndpoint;
|
|
1162
|
+
model;
|
|
1163
|
+
tokenCompanyModel;
|
|
1164
|
+
aggressiveness;
|
|
1165
|
+
tokenCompanyAggressiveness;
|
|
1166
|
+
tokenCompanyAppId;
|
|
1167
|
+
usageTapCompressionApiKey;
|
|
1168
|
+
usageTapCompressionEndpoint;
|
|
1169
|
+
usageTapCompressionMessagesEndpoint;
|
|
1170
|
+
usageTapCompressionModel;
|
|
1171
|
+
usageTapCompressionAggressiveness;
|
|
1172
|
+
sampling;
|
|
1173
|
+
samplingSettingsCacheMs;
|
|
1174
|
+
circuitBreaker;
|
|
1175
|
+
circuitBreakerRuns = /* @__PURE__ */ new Map();
|
|
1176
|
+
samplingSettingsCache;
|
|
1177
|
+
constructor(options = {}) {
|
|
1178
|
+
const apiKey = options.apiKey?.trim() || readEnvironmentVariable("USAGETAP_API_KEY");
|
|
1179
|
+
const baseUrl = options.baseUrl?.trim() || readEnvironmentVariable("USAGETAP_BASE_URL") || DEFAULT_BASE_URL;
|
|
1180
|
+
if (!apiKey) {
|
|
1181
|
+
throw new UsageTapError(
|
|
1182
|
+
"USAGETAP_BAD_REQUEST",
|
|
1183
|
+
"UsageTapClient requires an apiKey or the USAGETAP_API_KEY environment variable"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (HAS_WINDOW && !options.allowBrowser) {
|
|
1187
|
+
throw new UsageTapError(
|
|
1188
|
+
"USAGETAP_BROWSER_RUNTIME",
|
|
1189
|
+
"UsageTapClient is designed for server-side environments. Pass allowBrowser=true only for testing."
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
|
|
1193
|
+
if (typeof fetchCandidate !== "function") {
|
|
1194
|
+
throw new UsageTapError(
|
|
1195
|
+
"USAGETAP_NETWORK_ERROR",
|
|
1196
|
+
"A global fetch implementation was not found. Pass fetchImpl in UsageTapClientOptions."
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
const normalizedBaseUrl2 = normalizeBaseUrl(baseUrl);
|
|
1200
|
+
this.baseUrl = new URL(normalizedBaseUrl2);
|
|
1201
|
+
this.apiKey = apiKey;
|
|
1202
|
+
this.fetchImpl = wrapFetchImplementation(fetchCandidate, !options.fetchImpl);
|
|
1203
|
+
const resourceConfig = {
|
|
1204
|
+
apiKey,
|
|
1205
|
+
apiBaseUrl: normalizedBaseUrl2,
|
|
1206
|
+
gatewayBaseUrl: options.gatewayBaseUrl?.trim() || readEnvironmentVariable("USAGETAP_GATEWAY_URL"),
|
|
1207
|
+
fetchImpl: this.fetchImpl,
|
|
1208
|
+
headers: options.headers,
|
|
1209
|
+
sdkVersion: SDK_VERSION,
|
|
1210
|
+
idempotencyGenerator: options.idempotencyGenerator
|
|
1211
|
+
};
|
|
1212
|
+
this.gateway = new GatewayResource(resourceConfig);
|
|
1213
|
+
this.summarization = new SummarizationResource(resourceConfig);
|
|
1214
|
+
this.defaultFeature = options.defaultFeature;
|
|
1215
|
+
this.defaultTags = options.defaultTags?.length ? dedupeStrings(options.defaultTags) : void 0;
|
|
1216
|
+
this.defaultHeaders = options.headers ? normalizeHeaderDictionary(options.headers) : {};
|
|
1217
|
+
this.retryDefaults = resolveRetryOptions(options.retries);
|
|
1218
|
+
this.idempotencyGenerator = options.idempotencyGenerator ?? createIdempotencyKey;
|
|
1219
|
+
this.logFn = options.onLog;
|
|
1220
|
+
this.metricFn = options.onUsageMetric;
|
|
1221
|
+
this.authHeader = options.useApiKeyHeader ? API_KEY_HEADER : AUTH_HEADER;
|
|
1222
|
+
this.autoIdempotency = options.autoIdempotency ?? true;
|
|
1223
|
+
this.tokenCompanyApiKey = options.tokenCompanyApiKey;
|
|
1224
|
+
this.tokenCompanyEndpoint = options.tokenCompanyEndpoint;
|
|
1225
|
+
this.model = options.model;
|
|
1226
|
+
this.tokenCompanyModel = options.tokenCompanyModel;
|
|
1227
|
+
this.aggressiveness = options.aggressiveness;
|
|
1228
|
+
this.tokenCompanyAggressiveness = options.tokenCompanyAggressiveness;
|
|
1229
|
+
this.tokenCompanyAppId = options.tokenCompanyAppId;
|
|
1230
|
+
this.usageTapCompressionApiKey = options.usageTapCompressionApiKey ?? apiKey;
|
|
1231
|
+
this.usageTapCompressionEndpoint = options.usageTapCompressionEndpoint;
|
|
1232
|
+
this.usageTapCompressionMessagesEndpoint = options.usageTapCompressionMessagesEndpoint;
|
|
1233
|
+
this.usageTapCompressionModel = options.usageTapCompressionModel;
|
|
1234
|
+
this.usageTapCompressionAggressiveness = options.usageTapCompressionAggressiveness;
|
|
1235
|
+
this.sampling = options.sampling;
|
|
1236
|
+
this.samplingSettingsCacheMs = Number.isFinite(options.samplingSettingsCacheMs) ? Math.max(0, Number(options.samplingSettingsCacheMs)) : 5 * 60 * 1e3;
|
|
1237
|
+
if (options.circuitBreaker) {
|
|
1238
|
+
const maxCallsPerRun = options.circuitBreaker.maxCallsPerRun;
|
|
1239
|
+
if (!Number.isInteger(maxCallsPerRun) || maxCallsPerRun < 1) {
|
|
1240
|
+
throw new UsageTapError(
|
|
1241
|
+
"USAGETAP_BAD_REQUEST",
|
|
1242
|
+
"circuitBreaker.maxCallsPerRun must be a positive integer"
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const runInactivityMs = options.circuitBreaker.runInactivityMs ?? DEFAULT_RUN_INACTIVITY_MS;
|
|
1246
|
+
if (!Number.isFinite(runInactivityMs) || runInactivityMs < 1) {
|
|
1247
|
+
throw new UsageTapError(
|
|
1248
|
+
"USAGETAP_BAD_REQUEST",
|
|
1249
|
+
"circuitBreaker.runInactivityMs must be a positive number"
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
this.circuitBreaker = {
|
|
1253
|
+
maxCallsPerRun,
|
|
1254
|
+
runInactivityMs
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
shouldSample(request, policy = this.sampling || void 0) {
|
|
1259
|
+
if (!policy) return false;
|
|
1260
|
+
const rate = Math.min(1, Math.max(0, Number(policy.rate) || 0));
|
|
1261
|
+
if (rate <= 0) return false;
|
|
1262
|
+
const customerId = request.customerId?.trim();
|
|
1263
|
+
if (customerId && policy.customers?.exclude?.includes(customerId)) return false;
|
|
1264
|
+
const feature = request.feature?.trim();
|
|
1265
|
+
if (feature && policy.features?.exclude?.includes(feature)) return false;
|
|
1266
|
+
const included = policy.features?.include?.filter(Boolean) ?? [];
|
|
1267
|
+
if (included.length > 0 && (!feature || !included.includes(feature))) return false;
|
|
1268
|
+
const minimum = Math.max(0, Math.round(policy.minInputTokens ?? 0));
|
|
1269
|
+
if (minimum > 0 && estimatePromptTokens(request.input) < minimum) return false;
|
|
1270
|
+
return (policy.random ?? Math.random)() < rate;
|
|
1271
|
+
}
|
|
1272
|
+
async getSamplingSettings(options = {}) {
|
|
1273
|
+
const now = Date.now();
|
|
1274
|
+
if (!options.forceRefresh && this.samplingSettingsCache && this.samplingSettingsCache.expiresAtMs > now) {
|
|
1275
|
+
return {
|
|
1276
|
+
result: { status: "ACCEPTED", code: "SAMPLING_SETTINGS_CACHED" },
|
|
1277
|
+
data: this.samplingSettingsCache.settings,
|
|
1278
|
+
correlationId: options.correlationId ?? "local-cache"
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
const response = await this.requestGet(
|
|
1282
|
+
SAMPLING_SETTINGS_PATH,
|
|
1283
|
+
{
|
|
1284
|
+
signal: options.signal,
|
|
1285
|
+
headers: options.headers,
|
|
1286
|
+
retries: options.retries,
|
|
1287
|
+
correlationId: options.correlationId
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
const serverCacheMs = Math.max(0, Number(response.data.cacheSeconds) || 0) * 1e3;
|
|
1291
|
+
const cacheMs = Math.min(this.samplingSettingsCacheMs, serverCacheMs);
|
|
1292
|
+
this.samplingSettingsCache = {
|
|
1293
|
+
settings: response.data,
|
|
1294
|
+
expiresAtMs: now + cacheMs
|
|
1295
|
+
};
|
|
1296
|
+
return response;
|
|
1297
|
+
}
|
|
1298
|
+
async shouldSampleAsync(request, policy) {
|
|
1299
|
+
if (policy) return this.shouldSample(request, policy);
|
|
1300
|
+
if (this.sampling === false) return false;
|
|
1301
|
+
if (this.sampling) return this.shouldSample(request, this.sampling);
|
|
1302
|
+
try {
|
|
1303
|
+
const settings = await this.getSamplingSettings();
|
|
1304
|
+
return this.shouldSample(request, settings.data);
|
|
1305
|
+
} catch {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async decideSample(request, options = {}) {
|
|
1310
|
+
const hasTokens = Number.isFinite(request.inputTokens) && Number(request.inputTokens) >= 0;
|
|
1311
|
+
const hasCharacters = Number.isFinite(request.inputCharacters) && Number(request.inputCharacters) >= 0;
|
|
1312
|
+
if (!hasTokens && !hasCharacters) {
|
|
1313
|
+
throw new UsageTapError(
|
|
1314
|
+
"USAGETAP_BAD_REQUEST",
|
|
1315
|
+
"decideSample requires inputTokens or inputCharacters"
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
return this.request(
|
|
1319
|
+
SAMPLING_DECIDE_PATH,
|
|
1320
|
+
request,
|
|
1321
|
+
options
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
async captureSample(request, options = {}) {
|
|
1325
|
+
if (!request || request.input === void 0) {
|
|
1326
|
+
throw new UsageTapError(
|
|
1327
|
+
"USAGETAP_BAD_REQUEST",
|
|
1328
|
+
"captureSample requires input"
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
if (!request.provider?.trim()) {
|
|
1332
|
+
throw new UsageTapError(
|
|
1333
|
+
"USAGETAP_BAD_REQUEST",
|
|
1334
|
+
"captureSample requires provider"
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const sampleId = request.sampleId?.trim() || this.idempotencyGenerator();
|
|
1338
|
+
return this.request(
|
|
1339
|
+
SAMPLES_PATH,
|
|
1340
|
+
{ ...request, sampleId },
|
|
1341
|
+
{ ...options, idempotencyKey: sampleId }
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
async beginCall(request, options = {}) {
|
|
1345
|
+
const idempotencyKey = request.idempotencyKey ?? request.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
|
|
1346
|
+
this.reserveRunCall(request, idempotencyKey);
|
|
1347
|
+
const apiRequest = { ...request };
|
|
1348
|
+
delete apiRequest.runId;
|
|
1349
|
+
const payload = {
|
|
1350
|
+
...apiRequest,
|
|
1351
|
+
feature: request.feature ?? this.defaultFeature,
|
|
1352
|
+
tags: this.mergeTags(request.tags)
|
|
1353
|
+
};
|
|
1354
|
+
if (idempotencyKey) {
|
|
1355
|
+
payload.idempotencyKey = idempotencyKey;
|
|
1356
|
+
payload.idempotency = idempotencyKey;
|
|
1357
|
+
}
|
|
1358
|
+
const response = await this.request(
|
|
1359
|
+
CALL_BEGIN_PATH,
|
|
1360
|
+
payload,
|
|
1361
|
+
{
|
|
1362
|
+
...options,
|
|
1363
|
+
idempotencyKey
|
|
1364
|
+
}
|
|
1365
|
+
);
|
|
1366
|
+
return response;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Inspect a configured run circuit breaker without consuming another call.
|
|
1370
|
+
*/
|
|
1371
|
+
canRunContinue(request) {
|
|
1372
|
+
const identity = this.resolveRunIdentity(request);
|
|
1373
|
+
if (!identity || !this.circuitBreaker) {
|
|
1374
|
+
throw new UsageTapError(
|
|
1375
|
+
"USAGETAP_BAD_REQUEST",
|
|
1376
|
+
"canRunContinue requires circuitBreaker configuration and a non-empty runId"
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
this.expireInactiveRuns();
|
|
1380
|
+
const calls = this.circuitBreakerRuns.get(identity.key)?.calls ?? 0;
|
|
1381
|
+
return this.createCircuitBreakerDecision(identity.customerId, identity.runId, calls);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Release local state after a workflow finishes. Returns true when state existed.
|
|
1385
|
+
*/
|
|
1386
|
+
resetRun(request) {
|
|
1387
|
+
const identity = this.resolveRunIdentity(request);
|
|
1388
|
+
return identity ? this.circuitBreakerRuns.delete(identity.key) : false;
|
|
1389
|
+
}
|
|
1390
|
+
async promptCompress(request, options = {}) {
|
|
1391
|
+
if (!request?.callId) {
|
|
1392
|
+
throw new UsageTapError(
|
|
1393
|
+
"USAGETAP_BAD_REQUEST",
|
|
1394
|
+
"promptCompress requires callId"
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const requestInput = request.input ?? request.text;
|
|
1398
|
+
if (requestInput === void 0) {
|
|
1399
|
+
throw new UsageTapError(
|
|
1400
|
+
"USAGETAP_BAD_REQUEST",
|
|
1401
|
+
"promptCompress requires input or text"
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const result = await this.compressPromptInput(requestInput, {
|
|
1405
|
+
provider: request.provider,
|
|
1406
|
+
model: request.model,
|
|
1407
|
+
tokenCompanyModel: request.tokenCompanyModel,
|
|
1408
|
+
aggressiveness: request.aggressiveness,
|
|
1409
|
+
tokenCompanyAggressiveness: request.tokenCompanyAggressiveness,
|
|
1410
|
+
tokenCompanyAppId: request.tokenCompanyAppId,
|
|
1411
|
+
usageTapCompressionModel: request.usageTapCompressionModel,
|
|
1412
|
+
usageTapCompressionAggressiveness: request.usageTapCompressionAggressiveness,
|
|
1413
|
+
signal: options.signal
|
|
1414
|
+
});
|
|
1415
|
+
try {
|
|
1416
|
+
await this.recordPromptCompression(
|
|
1417
|
+
{
|
|
1418
|
+
callId: request.callId,
|
|
1419
|
+
promptCompression: this.toPromptCompressionTelemetry(result)
|
|
1420
|
+
},
|
|
1421
|
+
options
|
|
1422
|
+
);
|
|
1423
|
+
return { ...result, callId: request.callId };
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
return {
|
|
1426
|
+
...createPromptCompressionFallback(
|
|
1427
|
+
requestInput,
|
|
1428
|
+
request.provider ?? result.provider,
|
|
1429
|
+
error
|
|
1430
|
+
),
|
|
1431
|
+
callId: request.callId
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
async compressPromptInput(input, options = {}) {
|
|
1436
|
+
return compressPrompt({
|
|
1437
|
+
input,
|
|
1438
|
+
provider: options.provider,
|
|
1439
|
+
tokenCompanyApiKey: this.tokenCompanyApiKey,
|
|
1440
|
+
tokenCompanyEndpoint: this.tokenCompanyEndpoint,
|
|
1441
|
+
model: options.model ?? this.model,
|
|
1442
|
+
tokenCompanyModel: options.tokenCompanyModel ?? this.tokenCompanyModel,
|
|
1443
|
+
aggressiveness: options.aggressiveness ?? this.aggressiveness,
|
|
1444
|
+
tokenCompanyAggressiveness: options.tokenCompanyAggressiveness ?? this.tokenCompanyAggressiveness,
|
|
1445
|
+
tokenCompanyAppId: options.tokenCompanyAppId ?? this.tokenCompanyAppId,
|
|
1446
|
+
usageTapCompressionApiKey: this.usageTapCompressionApiKey,
|
|
1447
|
+
usageTapCompressionEndpoint: this.usageTapCompressionEndpoint,
|
|
1448
|
+
usageTapCompressionModel: options.usageTapCompressionModel ?? this.usageTapCompressionModel,
|
|
1449
|
+
usageTapCompressionAggressiveness: options.usageTapCompressionAggressiveness ?? this.usageTapCompressionAggressiveness,
|
|
1450
|
+
fetchImpl: this.fetchImpl,
|
|
1451
|
+
signal: options.signal,
|
|
1452
|
+
failOpen: options.failOpen
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Compress text with UsageTap's hosted compression service.
|
|
1457
|
+
*
|
|
1458
|
+
* This is the short, standalone path. It does not create a metered call and
|
|
1459
|
+
* fails open to the original text unless failOpen is explicitly disabled.
|
|
1460
|
+
*/
|
|
1461
|
+
async compress(text, options = {}) {
|
|
1462
|
+
if (typeof text !== "string") {
|
|
1463
|
+
throw new UsageTapError(
|
|
1464
|
+
"USAGETAP_BAD_REQUEST",
|
|
1465
|
+
"compress requires text"
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
const result = await this.compressPromptInput(text, {
|
|
1469
|
+
...options,
|
|
1470
|
+
provider: "usagetap"
|
|
1471
|
+
});
|
|
1472
|
+
const output = typeof result.compressedInput === "string" ? result.compressedInput : text;
|
|
1473
|
+
return {
|
|
1474
|
+
...result,
|
|
1475
|
+
compressedInput: output,
|
|
1476
|
+
output
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
async compressPromptMessages(input, options = {}) {
|
|
1480
|
+
return compressPromptMessages({
|
|
1481
|
+
input,
|
|
1482
|
+
provider: options.provider ?? "usagetap",
|
|
1483
|
+
usageTapCompressionApiKey: this.usageTapCompressionApiKey,
|
|
1484
|
+
usageTapCompressionMessagesEndpoint: this.usageTapCompressionMessagesEndpoint,
|
|
1485
|
+
aggressiveness: options.aggressiveness ?? this.aggressiveness,
|
|
1486
|
+
usageTapCompressionAggressiveness: options.usageTapCompressionAggressiveness ?? this.usageTapCompressionAggressiveness,
|
|
1487
|
+
mode: options.mode,
|
|
1488
|
+
latencyBudgetMs: options.latencyBudgetMs,
|
|
1489
|
+
compactEmptyUserMessages: options.compactEmptyUserMessages,
|
|
1490
|
+
compactDuplicateUserTextParts: options.compactDuplicateUserTextParts,
|
|
1491
|
+
fetchImpl: this.fetchImpl,
|
|
1492
|
+
signal: options.signal,
|
|
1493
|
+
failOpen: options.failOpen
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
async recordPromptCompression(request, options = {}) {
|
|
1497
|
+
if (!request?.callId) {
|
|
1498
|
+
throw new UsageTapError(
|
|
1499
|
+
"USAGETAP_BAD_REQUEST",
|
|
1500
|
+
"recordPromptCompression requires callId"
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
return this.request(
|
|
1504
|
+
COMPRESS_PROMPT_PATH,
|
|
1505
|
+
{
|
|
1506
|
+
callId: request.callId,
|
|
1507
|
+
promptCompression: request.promptCompression
|
|
1508
|
+
},
|
|
1509
|
+
options
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
async endCall(request, options = {}) {
|
|
1513
|
+
if (!request?.callId) {
|
|
1514
|
+
throw new UsageTapError(
|
|
1515
|
+
"USAGETAP_BAD_REQUEST",
|
|
1516
|
+
"endCall requires callId"
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const { customerId, feature, tags, ...apiPayload } = request;
|
|
1520
|
+
const response = await this.request(
|
|
1521
|
+
CALL_END_PATH,
|
|
1522
|
+
apiPayload,
|
|
1523
|
+
options
|
|
1524
|
+
);
|
|
1525
|
+
this.emitUsageMetric({
|
|
1526
|
+
type: "call_end",
|
|
1527
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1528
|
+
customerId: customerId ?? "unknown",
|
|
1529
|
+
callId: request.callId,
|
|
1530
|
+
feature: feature ?? this.defaultFeature,
|
|
1531
|
+
tags: tags ?? this.defaultTags,
|
|
1532
|
+
providerUsed: request.providerUsed,
|
|
1533
|
+
modelUsed: request.modelUsed,
|
|
1534
|
+
reasoningEffort: request.reasoningEffort,
|
|
1535
|
+
reasoningEffortSource: request.reasoningEffortSource,
|
|
1536
|
+
reasoningMode: request.reasoningMode,
|
|
1537
|
+
reasoningBudgetTokens: request.reasoningBudgetTokens,
|
|
1538
|
+
metrics: {
|
|
1539
|
+
inputTokens: request.inputTokens,
|
|
1540
|
+
responseTokens: request.responseTokens,
|
|
1541
|
+
cachedInputTokens: request.cachedInputTokens,
|
|
1542
|
+
cacheWriteInputTokens: request.cacheWriteInputTokens,
|
|
1543
|
+
cacheWrite5mInputTokens: request.cacheWrite5mInputTokens,
|
|
1544
|
+
cacheWrite1hInputTokens: request.cacheWrite1hInputTokens,
|
|
1545
|
+
reasoningTokens: request.reasoningTokens,
|
|
1546
|
+
searches: request.searches,
|
|
1547
|
+
audioSeconds: request.audioSeconds,
|
|
1548
|
+
imageInputCount: request.imageInputCount,
|
|
1549
|
+
imageInputTokens: request.imageInputTokens,
|
|
1550
|
+
imageOutputCount: request.imageOutputCount,
|
|
1551
|
+
imageOutputTokens: request.imageOutputTokens,
|
|
1552
|
+
audioInputTokens: request.audioInputTokens,
|
|
1553
|
+
cachedAudioInputTokens: request.cachedAudioInputTokens,
|
|
1554
|
+
audioOutputTokens: request.audioOutputTokens,
|
|
1555
|
+
costUsd: response.data.costUSD
|
|
1556
|
+
},
|
|
1557
|
+
correlationId: response.correlationId
|
|
1558
|
+
});
|
|
1559
|
+
return response;
|
|
1560
|
+
}
|
|
1561
|
+
async checkUsage(request, options = {}) {
|
|
1562
|
+
if (!request?.customerId) {
|
|
1563
|
+
throw new UsageTapError(
|
|
1564
|
+
"USAGETAP_BAD_REQUEST",
|
|
1565
|
+
"checkUsage requires customerId"
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
const path = CHECK_USAGE_PATH.replace(
|
|
1569
|
+
"{customerId}",
|
|
1570
|
+
encodeURIComponent(request.customerId)
|
|
1571
|
+
);
|
|
1572
|
+
const response = await this.requestGet(
|
|
1573
|
+
path,
|
|
1574
|
+
options
|
|
1575
|
+
);
|
|
1576
|
+
return response;
|
|
1577
|
+
}
|
|
1578
|
+
async createCustomer(request, options = {}) {
|
|
1579
|
+
if (!request?.customerId) {
|
|
1580
|
+
throw new UsageTapError(
|
|
1581
|
+
"USAGETAP_BAD_REQUEST",
|
|
1582
|
+
"createCustomer requires customerId"
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const idempotencyKey = options.idempotencyKey ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
|
|
1586
|
+
const response = await this.request(
|
|
1587
|
+
CREATE_CUSTOMER_PATH,
|
|
1588
|
+
{ ...request },
|
|
1589
|
+
{
|
|
1590
|
+
...options,
|
|
1591
|
+
idempotencyKey
|
|
1592
|
+
}
|
|
1593
|
+
);
|
|
1594
|
+
return response;
|
|
1595
|
+
}
|
|
1596
|
+
async changePlan(request, options = {}) {
|
|
1597
|
+
if (!request?.customerId) {
|
|
1598
|
+
throw new UsageTapError(
|
|
1599
|
+
"USAGETAP_BAD_REQUEST",
|
|
1600
|
+
"changePlan requires customerId"
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
if (!request?.planId) {
|
|
1604
|
+
throw new UsageTapError(
|
|
1605
|
+
"USAGETAP_BAD_REQUEST",
|
|
1606
|
+
"changePlan requires planId"
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
const path = CHANGE_PLAN_PATH.replace(
|
|
1610
|
+
"{customerId}",
|
|
1611
|
+
encodeURIComponent(request.customerId)
|
|
1612
|
+
);
|
|
1613
|
+
const idempotencyKey = options.idempotencyKey ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
|
|
1614
|
+
const payload = {
|
|
1615
|
+
planId: request.planId,
|
|
1616
|
+
strategy: request.strategy ?? "IMMEDIATE_RESET"
|
|
1617
|
+
};
|
|
1618
|
+
const response = await this.request(
|
|
1619
|
+
path,
|
|
1620
|
+
payload,
|
|
1621
|
+
{
|
|
1622
|
+
...options,
|
|
1623
|
+
idempotencyKey
|
|
1624
|
+
}
|
|
1625
|
+
);
|
|
1626
|
+
return response;
|
|
1627
|
+
}
|
|
1628
|
+
async incrementCustomMeter(request, options = {}) {
|
|
1629
|
+
if (!request?.customerId) {
|
|
1630
|
+
throw new UsageTapError(
|
|
1631
|
+
"USAGETAP_BAD_REQUEST",
|
|
1632
|
+
"incrementCustomMeter requires customerId"
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
if (!request?.meterSlot) {
|
|
1636
|
+
throw new UsageTapError(
|
|
1637
|
+
"USAGETAP_BAD_REQUEST",
|
|
1638
|
+
"incrementCustomMeter requires meterSlot"
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
if (!["CUSTOM1", "CUSTOM2", "AGENTIC_API"].includes(request.meterSlot)) {
|
|
1642
|
+
throw new UsageTapError(
|
|
1643
|
+
"USAGETAP_BAD_REQUEST",
|
|
1644
|
+
"meterSlot must be CUSTOM1, CUSTOM2 or AGENTIC_API"
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
if (typeof request.amount !== "number" || !Number.isFinite(request.amount) || request.amount <= 0) {
|
|
1648
|
+
throw new UsageTapError(
|
|
1649
|
+
"USAGETAP_BAD_REQUEST",
|
|
1650
|
+
"incrementCustomMeter requires a positive numeric amount"
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const idempotencyKey = options.idempotencyKey ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
|
|
1654
|
+
const payload = {
|
|
1655
|
+
customerId: request.customerId,
|
|
1656
|
+
meterSlot: request.meterSlot,
|
|
1657
|
+
amount: request.amount
|
|
1658
|
+
};
|
|
1659
|
+
if (request.customerUserId) {
|
|
1660
|
+
payload.customerUserId = request.customerUserId;
|
|
1661
|
+
}
|
|
1662
|
+
if (request.customerUserName) {
|
|
1663
|
+
payload.customerUserName = request.customerUserName;
|
|
1664
|
+
}
|
|
1665
|
+
if (request.customerUserEmail) {
|
|
1666
|
+
payload.customerUserEmail = request.customerUserEmail;
|
|
1667
|
+
}
|
|
1668
|
+
if (request.feature) {
|
|
1669
|
+
payload.feature = request.feature;
|
|
1670
|
+
}
|
|
1671
|
+
if (request.tags && request.tags.length > 0) {
|
|
1672
|
+
payload.tags = request.tags;
|
|
1673
|
+
}
|
|
1674
|
+
if (request.metadata) {
|
|
1675
|
+
payload.metadata = request.metadata;
|
|
1676
|
+
}
|
|
1677
|
+
const response = await this.request(
|
|
1678
|
+
INCREMENT_CUSTOM_METER_PATH,
|
|
1679
|
+
payload,
|
|
1680
|
+
{
|
|
1681
|
+
...options,
|
|
1682
|
+
idempotencyKey
|
|
1683
|
+
}
|
|
1684
|
+
);
|
|
1685
|
+
this.emitUsageMetric({
|
|
1686
|
+
type: "custom_meter",
|
|
1687
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1688
|
+
customerId: request.customerId,
|
|
1689
|
+
feature: request.feature ?? this.defaultFeature,
|
|
1690
|
+
tags: request.tags ?? this.defaultTags,
|
|
1691
|
+
metrics: {
|
|
1692
|
+
customMeterSlot: request.meterSlot,
|
|
1693
|
+
customMeterAmount: request.amount
|
|
1694
|
+
},
|
|
1695
|
+
correlationId: response.correlationId
|
|
1696
|
+
});
|
|
1697
|
+
return response;
|
|
1698
|
+
}
|
|
1699
|
+
async withUsage(beginRequest, handler, options = {}) {
|
|
1700
|
+
const idempotencyKey = beginRequest.idempotencyKey ?? beginRequest.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
|
|
1701
|
+
const beginPayload = idempotencyKey ? { ...beginRequest, idempotencyKey, idempotency: idempotencyKey } : { ...beginRequest };
|
|
1702
|
+
const beginResponse = await this.beginCall(beginPayload, options);
|
|
1703
|
+
let usage = {};
|
|
1704
|
+
const pricingMode = beginResponse.data.pricingMode ?? beginRequest.pricingMode ?? (beginRequest.batch === true ? "batch" : beginRequest.batch === false ? "standard" : void 0);
|
|
1705
|
+
if (pricingMode) {
|
|
1706
|
+
usage.pricingMode = pricingMode;
|
|
1707
|
+
usage.batch = pricingMode === "batch";
|
|
1708
|
+
}
|
|
1709
|
+
const initialStripeCustomerId = typeof beginResponse.data.stripeCustomerId === "string" ? beginResponse.data.stripeCustomerId : typeof beginRequest.stripeCustomerId === "string" ? beginRequest.stripeCustomerId : void 0;
|
|
1710
|
+
if (initialStripeCustomerId) {
|
|
1711
|
+
usage = { ...usage, stripeCustomerId: initialStripeCustomerId };
|
|
1712
|
+
}
|
|
1713
|
+
let errorPayload;
|
|
1714
|
+
let handlerResult;
|
|
1715
|
+
let handlerError;
|
|
1716
|
+
let endCallError;
|
|
1717
|
+
let finalizationDeferred = false;
|
|
1718
|
+
let finalizationPromise;
|
|
1719
|
+
const finalize = () => {
|
|
1720
|
+
if (!finalizationPromise) {
|
|
1721
|
+
finalizationPromise = this.endCall(
|
|
1722
|
+
{
|
|
1723
|
+
callId: beginResponse.data.callId,
|
|
1724
|
+
// Pass context for metric tracking
|
|
1725
|
+
customerId: beginRequest.customerId,
|
|
1726
|
+
feature: beginRequest.feature ?? this.defaultFeature,
|
|
1727
|
+
tags: beginRequest.tags ?? this.defaultTags,
|
|
1728
|
+
...usage,
|
|
1729
|
+
error: errorPayload
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
...options,
|
|
1733
|
+
correlationId: beginResponse.correlationId
|
|
1734
|
+
}
|
|
1735
|
+
).then(() => void 0);
|
|
1736
|
+
}
|
|
1737
|
+
return finalizationPromise;
|
|
1738
|
+
};
|
|
1739
|
+
const context = {
|
|
1740
|
+
begin: beginResponse,
|
|
1741
|
+
setUsage: (u) => {
|
|
1742
|
+
usage = { ...usage, ...u };
|
|
1743
|
+
},
|
|
1744
|
+
setError: (err) => {
|
|
1745
|
+
errorPayload = err;
|
|
1746
|
+
},
|
|
1747
|
+
deferFinalization: () => {
|
|
1748
|
+
finalizationDeferred = true;
|
|
1749
|
+
return finalize;
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
try {
|
|
1753
|
+
handlerResult = await handler(context);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
handlerError = error;
|
|
1756
|
+
if (!errorPayload) {
|
|
1757
|
+
errorPayload = {
|
|
1758
|
+
code: options.defaultErrorCode ?? "VENDOR_ERROR",
|
|
1759
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
} finally {
|
|
1763
|
+
if (handlerError || !finalizationDeferred) {
|
|
1764
|
+
try {
|
|
1765
|
+
await finalize();
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
endCallError = error;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (handlerError) {
|
|
1772
|
+
throw handlerError;
|
|
1773
|
+
}
|
|
1774
|
+
if (endCallError) {
|
|
1775
|
+
throw wrapEndCallError(endCallError, beginResponse.correlationId);
|
|
1776
|
+
}
|
|
1777
|
+
return handlerResult;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Meter one operation. Pass only a customer ID for the common path, or the
|
|
1781
|
+
* existing begin-call request object when feature, tags, or entitlements are needed.
|
|
1782
|
+
*/
|
|
1783
|
+
async meter(request, handler, options = {}) {
|
|
1784
|
+
const beginRequest = typeof request === "string" ? { customerId: request } : request;
|
|
1785
|
+
return this.withUsage(beginRequest, handler, options);
|
|
1786
|
+
}
|
|
1787
|
+
toPromptCompressionTelemetry(result) {
|
|
1788
|
+
return {
|
|
1789
|
+
provider: result.provider,
|
|
1790
|
+
originalTokens: result.originalTokens,
|
|
1791
|
+
compressedTokens: result.compressedTokens,
|
|
1792
|
+
savedTokens: result.savedTokens,
|
|
1793
|
+
tokenSavingsRatio: result.tokenSavingsRatio,
|
|
1794
|
+
techniques: result.techniques
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
reserveRunCall(request, idempotencyKey) {
|
|
1798
|
+
const identity = this.resolveRunIdentity(request);
|
|
1799
|
+
if (!identity || !this.circuitBreaker) return;
|
|
1800
|
+
this.expireInactiveRuns();
|
|
1801
|
+
const now = Date.now();
|
|
1802
|
+
const state = this.circuitBreakerRuns.get(identity.key) ?? {
|
|
1803
|
+
calls: 0,
|
|
1804
|
+
reservationKeys: /* @__PURE__ */ new Set(),
|
|
1805
|
+
lastSeenAtMs: now
|
|
1806
|
+
};
|
|
1807
|
+
const reservationKey = idempotencyKey ?? this.idempotencyGenerator();
|
|
1808
|
+
state.lastSeenAtMs = now;
|
|
1809
|
+
if (state.reservationKeys.has(reservationKey)) {
|
|
1810
|
+
this.circuitBreakerRuns.set(identity.key, state);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const decision = this.createCircuitBreakerDecision(
|
|
1814
|
+
identity.customerId,
|
|
1815
|
+
identity.runId,
|
|
1816
|
+
state.calls
|
|
1817
|
+
);
|
|
1818
|
+
if (!decision.allowed) {
|
|
1819
|
+
throw new UsageTapError(
|
|
1820
|
+
"USAGETAP_CIRCUIT_OPEN",
|
|
1821
|
+
`Run ${identity.runId} reached its ${decision.limit}-call circuit-breaker limit`,
|
|
1822
|
+
{
|
|
1823
|
+
details: {
|
|
1824
|
+
reason: decision.reason,
|
|
1825
|
+
customerId: identity.customerId,
|
|
1826
|
+
runId: identity.runId,
|
|
1827
|
+
calls: decision.calls,
|
|
1828
|
+
limit: decision.limit,
|
|
1829
|
+
remaining: decision.remaining
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
state.calls += 1;
|
|
1835
|
+
state.reservationKeys.add(reservationKey);
|
|
1836
|
+
this.circuitBreakerRuns.set(identity.key, state);
|
|
1837
|
+
}
|
|
1838
|
+
resolveRunIdentity(request) {
|
|
1839
|
+
const customerId = request.customerId?.trim();
|
|
1840
|
+
const runId = request.runId?.trim();
|
|
1841
|
+
if (!customerId || !runId) return void 0;
|
|
1842
|
+
return {
|
|
1843
|
+
key: `${customerId}\0${runId}`,
|
|
1844
|
+
customerId,
|
|
1845
|
+
runId
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
createCircuitBreakerDecision(customerId, runId, calls) {
|
|
1849
|
+
const limit = this.circuitBreaker?.maxCallsPerRun ?? 0;
|
|
1850
|
+
const allowed = calls < limit;
|
|
1851
|
+
return {
|
|
1852
|
+
allowed,
|
|
1853
|
+
...allowed ? {} : { reason: "max_calls_per_run" },
|
|
1854
|
+
customerId,
|
|
1855
|
+
runId,
|
|
1856
|
+
calls,
|
|
1857
|
+
limit,
|
|
1858
|
+
remaining: Math.max(0, limit - calls)
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
expireInactiveRuns() {
|
|
1862
|
+
if (!this.circuitBreaker || this.circuitBreakerRuns.size === 0) return;
|
|
1863
|
+
const expiredBefore = Date.now() - this.circuitBreaker.runInactivityMs;
|
|
1864
|
+
for (const [key, state] of this.circuitBreakerRuns) {
|
|
1865
|
+
if (state.lastSeenAtMs < expiredBefore) {
|
|
1866
|
+
this.circuitBreakerRuns.delete(key);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
async request(path, payload, options) {
|
|
1871
|
+
const url = new URL(path, this.baseUrl).toString();
|
|
1872
|
+
const body = payload !== void 0 ? JSON.stringify(payload) : void 0;
|
|
1873
|
+
const headers = this.composeHeaders(body, options);
|
|
1874
|
+
const resolvedRetry = resolveRetryOptions(
|
|
1875
|
+
this.retryDefaults,
|
|
1876
|
+
options.retries
|
|
1877
|
+
);
|
|
1878
|
+
const startTime = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1879
|
+
return runWithRetry(
|
|
1880
|
+
async (attempt) => {
|
|
1881
|
+
const startedAt = startTime();
|
|
1882
|
+
this.log({
|
|
1883
|
+
event: "request:start",
|
|
1884
|
+
path,
|
|
1885
|
+
attempt,
|
|
1886
|
+
idempotencyKey: options.idempotencyKey,
|
|
1887
|
+
correlationId: options.correlationId
|
|
1888
|
+
});
|
|
1889
|
+
const response = await this.performFetch({
|
|
1890
|
+
url,
|
|
1891
|
+
method: "POST",
|
|
1892
|
+
headers,
|
|
1893
|
+
body,
|
|
1894
|
+
signal: options.signal
|
|
1895
|
+
});
|
|
1896
|
+
this.log({
|
|
1897
|
+
event: "request:success",
|
|
1898
|
+
path,
|
|
1899
|
+
attempt,
|
|
1900
|
+
idempotencyKey: options.idempotencyKey,
|
|
1901
|
+
correlationId: response.correlationId,
|
|
1902
|
+
elapsedMs: startTime() - startedAt
|
|
1903
|
+
});
|
|
1904
|
+
return response;
|
|
1905
|
+
},
|
|
1906
|
+
resolvedRetry,
|
|
1907
|
+
(error) => this.shouldRetry(error),
|
|
1908
|
+
(attempt, delayMs, error) => {
|
|
1909
|
+
this.log({
|
|
1910
|
+
event: "retry:scheduled",
|
|
1911
|
+
path,
|
|
1912
|
+
attempt,
|
|
1913
|
+
idempotencyKey: options.idempotencyKey,
|
|
1914
|
+
correlationId: options.correlationId,
|
|
1915
|
+
error,
|
|
1916
|
+
elapsedMs: delayMs
|
|
1917
|
+
});
|
|
1918
|
+
},
|
|
1919
|
+
options.signal
|
|
1920
|
+
).catch((error) => {
|
|
1921
|
+
this.log({
|
|
1922
|
+
event: "retry:exhausted",
|
|
1923
|
+
path,
|
|
1924
|
+
attempt: resolvedRetry.maxAttempts,
|
|
1925
|
+
idempotencyKey: options.idempotencyKey,
|
|
1926
|
+
correlationId: options.correlationId,
|
|
1927
|
+
error
|
|
1928
|
+
});
|
|
1929
|
+
throw error;
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
async requestGet(path, options) {
|
|
1933
|
+
const url = new URL(path, this.baseUrl).toString();
|
|
1934
|
+
const headers = this.composeHeaders(void 0, options);
|
|
1935
|
+
const resolvedRetry = resolveRetryOptions(
|
|
1936
|
+
this.retryDefaults,
|
|
1937
|
+
options.retries
|
|
1938
|
+
);
|
|
1939
|
+
const startTime = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1940
|
+
return runWithRetry(
|
|
1941
|
+
async (attempt) => {
|
|
1942
|
+
const startedAt = startTime();
|
|
1943
|
+
this.log({
|
|
1944
|
+
event: "request:start",
|
|
1945
|
+
path,
|
|
1946
|
+
attempt,
|
|
1947
|
+
correlationId: options.correlationId
|
|
1948
|
+
});
|
|
1949
|
+
const response = await this.performFetch({
|
|
1950
|
+
url,
|
|
1951
|
+
method: "GET",
|
|
1952
|
+
headers,
|
|
1953
|
+
signal: options.signal
|
|
1954
|
+
});
|
|
1955
|
+
this.log({
|
|
1956
|
+
event: "request:success",
|
|
1957
|
+
path,
|
|
1958
|
+
attempt,
|
|
1959
|
+
correlationId: response.correlationId,
|
|
1960
|
+
elapsedMs: startTime() - startedAt
|
|
1961
|
+
});
|
|
1962
|
+
return response;
|
|
1963
|
+
},
|
|
1964
|
+
resolvedRetry,
|
|
1965
|
+
(error) => this.shouldRetry(error),
|
|
1966
|
+
(attempt, delayMs, error) => {
|
|
1967
|
+
this.log({
|
|
1968
|
+
event: "retry:scheduled",
|
|
1969
|
+
path,
|
|
1970
|
+
attempt,
|
|
1971
|
+
correlationId: options.correlationId,
|
|
1972
|
+
error,
|
|
1973
|
+
elapsedMs: delayMs
|
|
1974
|
+
});
|
|
1975
|
+
},
|
|
1976
|
+
options.signal
|
|
1977
|
+
).catch((error) => {
|
|
1978
|
+
this.log({
|
|
1979
|
+
event: "retry:exhausted",
|
|
1980
|
+
path,
|
|
1981
|
+
attempt: resolvedRetry.maxAttempts,
|
|
1982
|
+
correlationId: options.correlationId,
|
|
1983
|
+
error
|
|
1984
|
+
});
|
|
1985
|
+
throw error;
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
async performFetch(init) {
|
|
1989
|
+
let response;
|
|
1990
|
+
try {
|
|
1991
|
+
response = await this.fetchImpl(init.url, {
|
|
1992
|
+
method: init.method,
|
|
1993
|
+
headers: init.headers,
|
|
1994
|
+
body: init.body,
|
|
1995
|
+
signal: init.signal
|
|
1996
|
+
});
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
throw new UsageTapError(
|
|
1999
|
+
"USAGETAP_NETWORK_ERROR",
|
|
2000
|
+
"Failed to reach UsageTap",
|
|
2001
|
+
{
|
|
2002
|
+
retryable: true,
|
|
2003
|
+
cause: error
|
|
2004
|
+
}
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
const correlationId = response.headers.get(CORRELATION_HEADER) ?? void 0;
|
|
2008
|
+
const text = await response.text();
|
|
2009
|
+
let payload;
|
|
2010
|
+
if (text) {
|
|
2011
|
+
try {
|
|
2012
|
+
payload = JSON.parse(text);
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
throw new UsageTapError(
|
|
2015
|
+
"USAGETAP_INVALID_RESPONSE",
|
|
2016
|
+
"UsageTap returned invalid JSON",
|
|
2017
|
+
{
|
|
2018
|
+
retryable: false,
|
|
2019
|
+
correlationId,
|
|
2020
|
+
cause: error
|
|
2021
|
+
}
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (!response.ok) {
|
|
2026
|
+
throw this.toHttpError(response.status, payload, correlationId);
|
|
2027
|
+
}
|
|
2028
|
+
if (!payload?.result || payload.result.status !== "ACCEPTED") {
|
|
2029
|
+
throw this.toApiError(payload, correlationId);
|
|
2030
|
+
}
|
|
2031
|
+
const resolvedCorrelation = payload.correlationId ?? correlationId;
|
|
2032
|
+
if (payload.data === void 0 || payload.data === null || !resolvedCorrelation) {
|
|
2033
|
+
throw new UsageTapError(
|
|
2034
|
+
"USAGETAP_INVALID_RESPONSE",
|
|
2035
|
+
"UsageTap response missing data or correlationId",
|
|
2036
|
+
{
|
|
2037
|
+
correlationId: resolvedCorrelation ?? correlationId
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
result: {
|
|
2043
|
+
status: payload.result.status,
|
|
2044
|
+
code: payload.result.code,
|
|
2045
|
+
message: payload.result.message,
|
|
2046
|
+
timestamp: payload.result.timestamp
|
|
2047
|
+
},
|
|
2048
|
+
data: payload.data,
|
|
2049
|
+
correlationId: resolvedCorrelation
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
composeHeaders(body, options) {
|
|
2053
|
+
const headers = {
|
|
2054
|
+
...this.defaultHeaders,
|
|
2055
|
+
[SDK_HEADER]: `js/${SDK_VERSION}`,
|
|
2056
|
+
"content-type": "application/json",
|
|
2057
|
+
accept: CANONICAL_MEDIA_TYPE2
|
|
2058
|
+
};
|
|
2059
|
+
if (!HAS_WINDOW) {
|
|
2060
|
+
headers["user-agent"] = `${USER_AGENT}/${SDK_VERSION}`;
|
|
2061
|
+
}
|
|
2062
|
+
if (this.authHeader === API_KEY_HEADER) {
|
|
2063
|
+
headers[API_KEY_HEADER] = this.apiKey;
|
|
2064
|
+
} else {
|
|
2065
|
+
headers[AUTH_HEADER] = `Bearer ${this.apiKey}`;
|
|
2066
|
+
}
|
|
2067
|
+
if (options.idempotencyKey) {
|
|
2068
|
+
headers[IDEMPOTENCY_HEADER] = options.idempotencyKey;
|
|
2069
|
+
}
|
|
2070
|
+
if (options.correlationId) {
|
|
2071
|
+
headers[CORRELATION_HEADER] = options.correlationId;
|
|
2072
|
+
}
|
|
2073
|
+
if (!body) {
|
|
2074
|
+
delete headers["content-type"];
|
|
2075
|
+
}
|
|
2076
|
+
if (options.headers) {
|
|
2077
|
+
Object.assign(headers, normalizeHeaderDictionary(options.headers));
|
|
2078
|
+
}
|
|
2079
|
+
return headers;
|
|
2080
|
+
}
|
|
2081
|
+
log(entry) {
|
|
2082
|
+
this.logFn?.(entry);
|
|
2083
|
+
}
|
|
2084
|
+
emitUsageMetric(event) {
|
|
2085
|
+
try {
|
|
2086
|
+
this.metricFn?.(event);
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
mergeTags(tags) {
|
|
2091
|
+
if (!tags && !this.defaultTags) {
|
|
2092
|
+
return void 0;
|
|
2093
|
+
}
|
|
2094
|
+
const combined = [...this.defaultTags ?? [], ...tags ?? []].filter(
|
|
2095
|
+
Boolean
|
|
2096
|
+
);
|
|
2097
|
+
return combined.length ? dedupeStrings(combined) : void 0;
|
|
2098
|
+
}
|
|
2099
|
+
shouldRetry(error) {
|
|
2100
|
+
if (isUsageTapError(error)) {
|
|
2101
|
+
return Boolean(error.retryable);
|
|
2102
|
+
}
|
|
2103
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
2108
|
+
toHttpError(status, payload, correlationId) {
|
|
2109
|
+
const code = mapStatusToErrorCode(status);
|
|
2110
|
+
const apiCode = payload?.error?.code ?? payload?.result?.code ?? "UNKNOWN";
|
|
2111
|
+
const retryable = isRetryableStatus(status) || isRetryableApiCode(apiCode);
|
|
2112
|
+
const message = payload?.error?.message ?? payload?.result?.message ?? `UsageTap responded with HTTP ${status}`;
|
|
2113
|
+
return new UsageTapError(code, message, {
|
|
2114
|
+
status,
|
|
2115
|
+
retryable,
|
|
2116
|
+
correlationId: payload?.correlationId ?? correlationId,
|
|
2117
|
+
details: sanitizeDetails(payload)
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
toApiError(payload, correlationId) {
|
|
2121
|
+
const normalizedCode = payload?.error?.code ?? payload?.result?.code ?? "UNKNOWN";
|
|
2122
|
+
const retryable = isRetryableApiCode(normalizedCode);
|
|
2123
|
+
const message = payload?.error?.message ?? payload?.result?.message ?? "UsageTap reported an error";
|
|
2124
|
+
return new UsageTapError(mapApiCodeToError(normalizedCode), message, {
|
|
2125
|
+
retryable,
|
|
2126
|
+
correlationId: payload?.correlationId ?? correlationId,
|
|
2127
|
+
details: sanitizeDetails(payload)
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
function mapStatusToErrorCode(status) {
|
|
2132
|
+
if (status === 401 || status === 403) return "USAGETAP_AUTH_ERROR";
|
|
2133
|
+
if (status === 400 || status === 404 || status === 409)
|
|
2134
|
+
return "USAGETAP_BAD_REQUEST";
|
|
2135
|
+
if (status === 429) return "USAGETAP_RATE_LIMITED";
|
|
2136
|
+
if (status >= 500) return "USAGETAP_SERVER_ERROR";
|
|
2137
|
+
return "USAGETAP_INVALID_RESPONSE";
|
|
2138
|
+
}
|
|
2139
|
+
function isRetryableStatus(status) {
|
|
2140
|
+
return status === 408 || status === 425 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
2141
|
+
}
|
|
2142
|
+
function isRetryableApiCode(code) {
|
|
2143
|
+
const normalized = code.toUpperCase();
|
|
2144
|
+
return normalized === "PAYG_CONFLICT" || normalized.includes("TRANSIENT") || normalized.includes("RETRY") || normalized.includes("TIMEOUT") || normalized.includes("THROTTLE") || normalized.includes("RATE_LIMIT");
|
|
2145
|
+
}
|
|
2146
|
+
function mapApiCodeToError(code) {
|
|
2147
|
+
const normalized = code.toUpperCase();
|
|
2148
|
+
if (normalized.includes("AUTH") || normalized.includes("TOKEN")) {
|
|
2149
|
+
return "USAGETAP_AUTH_ERROR";
|
|
2150
|
+
}
|
|
2151
|
+
if (normalized.includes("RATE") || normalized.includes("THROTTLE")) {
|
|
2152
|
+
return "USAGETAP_RATE_LIMITED";
|
|
2153
|
+
}
|
|
2154
|
+
if (normalized.includes("SERVER") || normalized.includes("TRANSIENT")) {
|
|
2155
|
+
return "USAGETAP_SERVER_ERROR";
|
|
2156
|
+
}
|
|
2157
|
+
if (normalized.includes("IDEMPOTENCY") || normalized.includes("VALIDATION") || normalized.includes("REQUEST")) {
|
|
2158
|
+
return "USAGETAP_BAD_REQUEST";
|
|
2159
|
+
}
|
|
2160
|
+
return "USAGETAP_INVALID_RESPONSE";
|
|
2161
|
+
}
|
|
2162
|
+
function sanitizeDetails(payload) {
|
|
2163
|
+
if (!payload) return void 0;
|
|
2164
|
+
const details = {};
|
|
2165
|
+
if (payload.result) details.result = payload.result;
|
|
2166
|
+
if (payload.error) details.error = payload.error;
|
|
2167
|
+
return Object.keys(details).length ? details : void 0;
|
|
2168
|
+
}
|
|
2169
|
+
function readEnvironmentVariable(name) {
|
|
2170
|
+
const runtime = globalThis;
|
|
2171
|
+
const value = runtime.process?.env?.[name]?.trim();
|
|
2172
|
+
return value || void 0;
|
|
2173
|
+
}
|
|
2174
|
+
function normalizeBaseUrl(baseUrl) {
|
|
2175
|
+
const trimmed = baseUrl.trim();
|
|
2176
|
+
if (!trimmed) return trimmed;
|
|
2177
|
+
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
2178
|
+
}
|
|
2179
|
+
function normalizeHeaderDictionary(dict) {
|
|
2180
|
+
return Object.keys(dict).reduce((acc, key) => {
|
|
2181
|
+
acc[key.toLowerCase()] = dict[key];
|
|
2182
|
+
return acc;
|
|
2183
|
+
}, {});
|
|
2184
|
+
}
|
|
2185
|
+
function dedupeStrings(values) {
|
|
2186
|
+
return Array.from(
|
|
2187
|
+
new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
function wrapFetchImplementation(fetchCandidate, preferGlobalContext) {
|
|
2191
|
+
const target = preferGlobalContext ? globalThis : void 0;
|
|
2192
|
+
return ((...args) => target ? Reflect.apply(fetchCandidate, target, args) : fetchCandidate(...args));
|
|
2193
|
+
}
|
|
2194
|
+
function wrapEndCallError(error, correlationId) {
|
|
2195
|
+
if (isUsageTapError(error)) {
|
|
2196
|
+
return new UsageTapError("USAGETAP_END_CALL_ERROR", error.message, {
|
|
2197
|
+
correlationId: error.correlationId ?? correlationId,
|
|
2198
|
+
details: error.details,
|
|
2199
|
+
cause: error
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
return new UsageTapError(
|
|
2203
|
+
"USAGETAP_END_CALL_ERROR",
|
|
2204
|
+
"Failed to finalize UsageTap call",
|
|
2205
|
+
{
|
|
2206
|
+
correlationId,
|
|
2207
|
+
cause: error
|
|
2208
|
+
}
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
1
2212
|
// src/adapters/openai.ts
|
|
2213
|
+
var OpenAIPromptCompressionStats = class {
|
|
2214
|
+
history = [];
|
|
2215
|
+
failures = [];
|
|
2216
|
+
_record(turn) {
|
|
2217
|
+
this.history.push(turn);
|
|
2218
|
+
}
|
|
2219
|
+
_recordFailure(failure) {
|
|
2220
|
+
this.failures.push(failure);
|
|
2221
|
+
}
|
|
2222
|
+
get totalOriginalTokens() {
|
|
2223
|
+
return this.history.reduce((sum, turn) => sum + (turn.originalTokens ?? 0), 0);
|
|
2224
|
+
}
|
|
2225
|
+
get totalCompressedTokens() {
|
|
2226
|
+
return this.history.reduce((sum, turn) => sum + (turn.compressedTokens ?? 0), 0);
|
|
2227
|
+
}
|
|
2228
|
+
get totalTokensSaved() {
|
|
2229
|
+
return this.history.reduce((sum, turn) => sum + (turn.savedTokens ?? 0), 0);
|
|
2230
|
+
}
|
|
2231
|
+
get totalOriginalCharacters() {
|
|
2232
|
+
return this.history.reduce((sum, turn) => sum + turn.originalCharacters, 0);
|
|
2233
|
+
}
|
|
2234
|
+
get totalCompressedCharacters() {
|
|
2235
|
+
return this.history.reduce((sum, turn) => sum + turn.compressedCharacters, 0);
|
|
2236
|
+
}
|
|
2237
|
+
get totalCharactersSaved() {
|
|
2238
|
+
return this.history.reduce((sum, turn) => sum + turn.savedCharacters, 0);
|
|
2239
|
+
}
|
|
2240
|
+
get calls() {
|
|
2241
|
+
return this.history.length;
|
|
2242
|
+
}
|
|
2243
|
+
get telemetryFailures() {
|
|
2244
|
+
return this.failures.length;
|
|
2245
|
+
}
|
|
2246
|
+
get failOpenEvents() {
|
|
2247
|
+
return this.history.filter(
|
|
2248
|
+
(turn) => turn.techniques.includes("compression-error") || turn.techniques.includes("fallback-original")
|
|
2249
|
+
).length;
|
|
2250
|
+
}
|
|
2251
|
+
get tokenSavingsRatio() {
|
|
2252
|
+
return this.totalOriginalTokens > 0 ? this.totalTokensSaved / this.totalOriginalTokens : 0;
|
|
2253
|
+
}
|
|
2254
|
+
get savingsRatio() {
|
|
2255
|
+
return this.totalOriginalCharacters > 0 ? this.totalCharactersSaved / this.totalOriginalCharacters : 0;
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2
2258
|
function createOpenAIAdapter(init) {
|
|
3
|
-
const { client, usageTap } = init;
|
|
2259
|
+
const { client, usageTap, provider = "openai" } = init;
|
|
4
2260
|
return {
|
|
5
2261
|
async invoke(params) {
|
|
6
2262
|
const result = await usageTap.withUsage(
|
|
7
2263
|
params.begin,
|
|
8
2264
|
async (ctx) => {
|
|
2265
|
+
ctx.setUsage({ providerUsed: provider });
|
|
9
2266
|
const response = await params.call(client, {
|
|
10
2267
|
hints: ctx.begin.data.vendorHints,
|
|
11
2268
|
begin: ctx.begin
|
|
12
2269
|
});
|
|
13
|
-
tryInferUsage(response, ctx.begin.data.vendorHints, params.extractUsage, ctx);
|
|
2270
|
+
tryInferUsage(response, ctx.begin.data.vendorHints, params.extractUsage, ctx, provider);
|
|
14
2271
|
return {
|
|
15
2272
|
data: response,
|
|
16
2273
|
begin: ctx.begin
|
|
@@ -24,16 +2281,576 @@ function createOpenAIAdapter(init) {
|
|
|
24
2281
|
const result = await usageTap.withUsage(
|
|
25
2282
|
params.begin,
|
|
26
2283
|
async (ctx) => {
|
|
2284
|
+
const settle = deferUsageFinalization(ctx);
|
|
2285
|
+
ctx.setUsage({ providerUsed: provider });
|
|
27
2286
|
const { stream, onComplete } = await params.call(client, {
|
|
28
2287
|
hints: ctx.begin.data.vendorHints,
|
|
29
2288
|
begin: ctx.begin
|
|
30
2289
|
});
|
|
31
|
-
const wrapped = wrapStreamForUsageTap(stream, async () => {
|
|
32
|
-
|
|
2290
|
+
const wrapped = wrapStreamForUsageTap(stream, async (termination) => {
|
|
2291
|
+
try {
|
|
2292
|
+
if (termination === "complete" && onComplete) {
|
|
2293
|
+
const maybeUsage = await onComplete();
|
|
2294
|
+
if (maybeUsage) {
|
|
2295
|
+
ctx.setUsage(maybeUsage);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
ctx.setError({
|
|
2300
|
+
code: "USAGE_FINALIZE_ERROR",
|
|
2301
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2302
|
+
});
|
|
2303
|
+
await settle();
|
|
2304
|
+
throw error;
|
|
2305
|
+
}
|
|
2306
|
+
await settle();
|
|
2307
|
+
}, ctx);
|
|
2308
|
+
const finalize = async () => {
|
|
2309
|
+
await wrapped.__usageTapFinalize?.();
|
|
2310
|
+
};
|
|
2311
|
+
return {
|
|
2312
|
+
stream: wrapped,
|
|
2313
|
+
begin: ctx.begin,
|
|
2314
|
+
finalize
|
|
2315
|
+
};
|
|
2316
|
+
},
|
|
2317
|
+
params.withUsageOptions
|
|
2318
|
+
);
|
|
2319
|
+
return result;
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
function toNextResponse(stream, options = {}) {
|
|
2324
|
+
const mode = options.mode ?? "text";
|
|
2325
|
+
const headers = new Headers(options.headers ?? {});
|
|
2326
|
+
if (mode === "sse") {
|
|
2327
|
+
headers.set("content-type", "text/event-stream; charset=utf-8");
|
|
2328
|
+
headers.set("cache-control", "no-cache, no-transform");
|
|
2329
|
+
headers.set("connection", "keep-alive");
|
|
2330
|
+
headers.set("x-accel-buffering", "no");
|
|
2331
|
+
} else {
|
|
2332
|
+
headers.set("content-type", options.contentType ?? "text/plain; charset=utf-8");
|
|
2333
|
+
}
|
|
2334
|
+
const encoder = new TextEncoder();
|
|
2335
|
+
let iterator;
|
|
2336
|
+
const body = new ReadableStream({
|
|
2337
|
+
async start(controller) {
|
|
2338
|
+
try {
|
|
2339
|
+
const getIterator = stream[Symbol.asyncIterator];
|
|
2340
|
+
if (typeof getIterator !== "function") {
|
|
2341
|
+
controller.close();
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
iterator = getIterator.call(stream);
|
|
2345
|
+
while (true) {
|
|
2346
|
+
const result = await iterator.next();
|
|
2347
|
+
if (result.done) {
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
const text = chunkToText(result.value);
|
|
2351
|
+
if (!text) {
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
if (mode === "sse") {
|
|
2355
|
+
controller.enqueue(encoder.encode(formatSsePayload(text, options.sse)));
|
|
2356
|
+
} else {
|
|
2357
|
+
controller.enqueue(encoder.encode(text));
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
controller.close();
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
controller.error(error);
|
|
2363
|
+
} finally {
|
|
2364
|
+
await stream.__usageTapFinalize?.();
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
async cancel() {
|
|
2368
|
+
if (!iterator) {
|
|
2369
|
+
const getIterator = stream[Symbol.asyncIterator];
|
|
2370
|
+
if (typeof getIterator === "function") {
|
|
2371
|
+
iterator = getIterator.call(stream);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
if (iterator && typeof iterator.return === "function") {
|
|
2375
|
+
await iterator.return();
|
|
2376
|
+
}
|
|
2377
|
+
await stream.__usageTapFinalize?.();
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
return new Response(body, { headers });
|
|
2381
|
+
}
|
|
2382
|
+
async function pipeToResponse(stream, res, options = {}) {
|
|
2383
|
+
const mode = options.mode ?? "text";
|
|
2384
|
+
if (mode === "sse") {
|
|
2385
|
+
setHeaderIfPossible(res, "Content-Type", "text/event-stream; charset=utf-8");
|
|
2386
|
+
setHeaderIfPossible(res, "Cache-Control", "no-cache, no-transform");
|
|
2387
|
+
setHeaderIfPossible(res, "Connection", "keep-alive");
|
|
2388
|
+
setHeaderIfPossible(res, "X-Accel-Buffering", "no");
|
|
2389
|
+
} else {
|
|
2390
|
+
setHeaderIfPossible(res, "Content-Type", options.contentType ?? "text/plain; charset=utf-8");
|
|
2391
|
+
}
|
|
2392
|
+
const encoder = new TextEncoder();
|
|
2393
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
2394
|
+
try {
|
|
2395
|
+
while (true) {
|
|
2396
|
+
const result = await iterator.next();
|
|
2397
|
+
if (result.done) {
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
const text = chunkToText(result.value);
|
|
2401
|
+
if (!text) {
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
const payload = mode === "sse" ? formatSsePayload(text, options.sse) : text;
|
|
2405
|
+
res.write(Buffer.from(encoder.encode(payload)));
|
|
2406
|
+
res.flush?.();
|
|
2407
|
+
}
|
|
2408
|
+
} finally {
|
|
2409
|
+
res.end();
|
|
2410
|
+
await stream.__usageTapFinalize?.();
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
var USAGETAP_CORRELATION_HEADER = "x-usage-correlation-id";
|
|
2414
|
+
function withSampling(client, options = {}) {
|
|
2415
|
+
if (!client || !options) {
|
|
2416
|
+
throw new UsageTapError(
|
|
2417
|
+
"USAGETAP_BAD_REQUEST",
|
|
2418
|
+
"withSampling requires an OpenAI-compatible client and sampling options"
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
const { apiKey, usageTapClient, provider = "openai", ...policy } = options;
|
|
2422
|
+
const localPolicy = typeof policy.rate === "number" ? policy : void 0;
|
|
2423
|
+
const usageTap = usageTapClient ?? new UsageTapClient({ apiKey, sampling: localPolicy });
|
|
2424
|
+
const wrapCreate = (create) => async (params, requestOptions) => {
|
|
2425
|
+
const { usageTap: callContextRaw, ...providerOptions } = requestOptions ?? {};
|
|
2426
|
+
const callContext = isObjectRecord(callContextRaw) ? callContextRaw : {};
|
|
2427
|
+
const streaming = params.stream === true;
|
|
2428
|
+
const decision = streaming ? Promise.resolve(false) : usageTap.shouldSampleAsync({
|
|
2429
|
+
customerId: readString(callContext.customerId),
|
|
2430
|
+
feature: readString(callContext.feature),
|
|
2431
|
+
input: params
|
|
2432
|
+
}, localPolicy);
|
|
2433
|
+
const startedAt = Date.now();
|
|
2434
|
+
try {
|
|
2435
|
+
const response = await create(
|
|
2436
|
+
params,
|
|
2437
|
+
Object.keys(providerOptions).length ? providerOptions : void 0
|
|
2438
|
+
);
|
|
2439
|
+
const selected = await decision;
|
|
2440
|
+
if (selected) {
|
|
2441
|
+
const record = isObjectRecord(response) ? response : {};
|
|
2442
|
+
await usageTap.captureSample({
|
|
2443
|
+
customerId: readString(callContext.customerId),
|
|
2444
|
+
feature: readString(callContext.feature),
|
|
2445
|
+
environment: readString(callContext.environment),
|
|
2446
|
+
tags: readStringArray(callContext.tags),
|
|
2447
|
+
provider,
|
|
2448
|
+
model: readString(record.model) ?? readString(params.model),
|
|
2449
|
+
input: params,
|
|
2450
|
+
output: response,
|
|
2451
|
+
usage: record.usage,
|
|
2452
|
+
latencyMs: Date.now() - startedAt
|
|
2453
|
+
}).catch(() => void 0);
|
|
2454
|
+
}
|
|
2455
|
+
return response;
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
const selected = await decision;
|
|
2458
|
+
if (selected) {
|
|
2459
|
+
await usageTap.captureSample({
|
|
2460
|
+
customerId: readString(callContext.customerId),
|
|
2461
|
+
feature: readString(callContext.feature),
|
|
2462
|
+
environment: readString(callContext.environment),
|
|
2463
|
+
tags: readStringArray(callContext.tags),
|
|
2464
|
+
provider,
|
|
2465
|
+
model: readString(params.model),
|
|
2466
|
+
input: params,
|
|
2467
|
+
latencyMs: Date.now() - startedAt,
|
|
2468
|
+
error: serializeSamplingError(error)
|
|
2469
|
+
}).catch(() => void 0);
|
|
2470
|
+
}
|
|
2471
|
+
throw error;
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
const chat = client.chat?.completions ? new Proxy(client.chat, {
|
|
2475
|
+
get(target, prop, receiver) {
|
|
2476
|
+
if (prop !== "completions") return safeReflectGet(target, prop, receiver);
|
|
2477
|
+
const completions = target.completions;
|
|
2478
|
+
return new Proxy(completions, {
|
|
2479
|
+
get(completionTarget, completionProp, completionReceiver) {
|
|
2480
|
+
if (completionProp === "create") {
|
|
2481
|
+
return wrapCreate(
|
|
2482
|
+
completionTarget.create.bind(completionTarget)
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
return safeReflectGet(
|
|
2486
|
+
completionTarget,
|
|
2487
|
+
completionProp,
|
|
2488
|
+
completionReceiver
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
}) : void 0;
|
|
2494
|
+
const responses = typeof client.responses !== "undefined" && client.responses ? new Proxy(client.responses, {
|
|
2495
|
+
get(target, prop, receiver) {
|
|
2496
|
+
if (prop === "create") {
|
|
2497
|
+
const create = Reflect.get(target, prop, receiver);
|
|
2498
|
+
return wrapCreate(create.bind(target));
|
|
2499
|
+
}
|
|
2500
|
+
return safeReflectGet(target, prop, receiver);
|
|
2501
|
+
}
|
|
2502
|
+
}) : void 0;
|
|
2503
|
+
return new Proxy(client, {
|
|
2504
|
+
get(target, prop, receiver) {
|
|
2505
|
+
if (prop === "chat" && chat) return chat;
|
|
2506
|
+
if (prop === "responses" && responses) return responses;
|
|
2507
|
+
if (prop === "unwrap") return () => target;
|
|
2508
|
+
return safeReflectGet(target, prop, receiver);
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
function safeReflectGet(target, prop, receiver) {
|
|
2513
|
+
return Reflect.get(target, prop, receiver);
|
|
2514
|
+
}
|
|
2515
|
+
function readStringArray(value) {
|
|
2516
|
+
if (!Array.isArray(value)) return void 0;
|
|
2517
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
2518
|
+
return strings.length ? strings : void 0;
|
|
2519
|
+
}
|
|
2520
|
+
function readString(value) {
|
|
2521
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
2522
|
+
}
|
|
2523
|
+
function serializeSamplingError(error) {
|
|
2524
|
+
if (error instanceof Error) {
|
|
2525
|
+
return { name: error.name, message: error.message };
|
|
2526
|
+
}
|
|
2527
|
+
return { message: String(error) };
|
|
2528
|
+
}
|
|
2529
|
+
function normalizeMeteredOpenAISampling(options) {
|
|
2530
|
+
if (!options) return void 0;
|
|
2531
|
+
if (options === true) return { provider: "openai" };
|
|
2532
|
+
const { provider = "openai", ...policyFields } = options;
|
|
2533
|
+
return {
|
|
2534
|
+
provider,
|
|
2535
|
+
policy: typeof policyFields.rate === "number" ? policyFields : void 0
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
function startMeteredOpenAISampleDecision({
|
|
2539
|
+
usageTap,
|
|
2540
|
+
sampling,
|
|
2541
|
+
beginRequest,
|
|
2542
|
+
input
|
|
2543
|
+
}) {
|
|
2544
|
+
if (!sampling) return Promise.resolve(false);
|
|
2545
|
+
return usageTap.shouldSampleAsync(
|
|
2546
|
+
{
|
|
2547
|
+
customerId: beginRequest.customerId,
|
|
2548
|
+
feature: beginRequest.feature,
|
|
2549
|
+
input
|
|
2550
|
+
},
|
|
2551
|
+
sampling.policy
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
async function captureMeteredOpenAISample({
|
|
2555
|
+
usageTap,
|
|
2556
|
+
sampling,
|
|
2557
|
+
decision,
|
|
2558
|
+
ctx,
|
|
2559
|
+
beginRequest,
|
|
2560
|
+
input,
|
|
2561
|
+
response,
|
|
2562
|
+
error,
|
|
2563
|
+
startedAt
|
|
2564
|
+
}) {
|
|
2565
|
+
if (!sampling) return;
|
|
2566
|
+
let selected = false;
|
|
2567
|
+
try {
|
|
2568
|
+
selected = await decision;
|
|
2569
|
+
} catch {
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (!selected) return;
|
|
2573
|
+
const record = isObjectRecord(response) ? response : {};
|
|
2574
|
+
await usageTap.captureSample({
|
|
2575
|
+
sampleId: ctx.begin.data.callId,
|
|
2576
|
+
callId: ctx.begin.data.callId,
|
|
2577
|
+
customerId: beginRequest.customerId,
|
|
2578
|
+
feature: beginRequest.feature,
|
|
2579
|
+
tags: beginRequest.tags,
|
|
2580
|
+
provider: sampling.provider,
|
|
2581
|
+
model: readString(record.model) ?? readString(input.model),
|
|
2582
|
+
input,
|
|
2583
|
+
...response === void 0 ? {} : { output: response },
|
|
2584
|
+
usage: record.usage,
|
|
2585
|
+
latencyMs: Date.now() - startedAt,
|
|
2586
|
+
...error === void 0 ? {} : { error: serializeSamplingError(error) }
|
|
2587
|
+
}).catch(() => void 0);
|
|
2588
|
+
}
|
|
2589
|
+
function withMetering(client, customer) {
|
|
2590
|
+
const config = typeof customer === "string" ? { customerId: customer } : customer;
|
|
2591
|
+
if (!config?.customerId) {
|
|
2592
|
+
throw new UsageTapError(
|
|
2593
|
+
"USAGETAP_BAD_REQUEST",
|
|
2594
|
+
"withMetering requires a customerId"
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
const {
|
|
2598
|
+
apiKey,
|
|
2599
|
+
usageTapClient,
|
|
2600
|
+
applyVendorHints,
|
|
2601
|
+
promptCompression,
|
|
2602
|
+
sampling,
|
|
2603
|
+
provider,
|
|
2604
|
+
...defaultContext
|
|
2605
|
+
} = config;
|
|
2606
|
+
const usageTap = usageTapClient ?? new UsageTapClient({ apiKey });
|
|
2607
|
+
const normalizedCompression = promptCompression === true ? { provider: "usagetap" } : promptCompression ? { provider: "usagetap", ...promptCompression } : void 0;
|
|
2608
|
+
return wrapOpenAI(client, usageTap, {
|
|
2609
|
+
defaultContext,
|
|
2610
|
+
applyVendorHints,
|
|
2611
|
+
promptCompression: normalizedCompression,
|
|
2612
|
+
sampling,
|
|
2613
|
+
provider
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
function wrapOpenAI(client, usageTap, options = {}) {
|
|
2617
|
+
if (!client) {
|
|
2618
|
+
throw new UsageTapError("USAGETAP_BAD_REQUEST", "wrapOpenAI requires an OpenAI client instance");
|
|
2619
|
+
}
|
|
2620
|
+
const defaultContext = options.defaultContext;
|
|
2621
|
+
const applyVendorHints = options.applyVendorHints !== false;
|
|
2622
|
+
const defaultPromptCompression = normalizePromptCompressionOptions(options.promptCompression);
|
|
2623
|
+
const defaultSampling = normalizeMeteredOpenAISampling(options.sampling);
|
|
2624
|
+
const provider = options.provider ?? "openai";
|
|
2625
|
+
const promptCompressionStats = new OpenAIPromptCompressionStats();
|
|
2626
|
+
const proxiedChat = client.chat ? createChatProxy(
|
|
2627
|
+
client.chat,
|
|
2628
|
+
usageTap,
|
|
2629
|
+
defaultContext,
|
|
2630
|
+
applyVendorHints,
|
|
2631
|
+
defaultPromptCompression,
|
|
2632
|
+
promptCompressionStats,
|
|
2633
|
+
defaultSampling,
|
|
2634
|
+
provider
|
|
2635
|
+
) : void 0;
|
|
2636
|
+
const proxiedResponses = typeof client.responses !== "undefined" ? createResponsesProxy(
|
|
2637
|
+
client.responses,
|
|
2638
|
+
usageTap,
|
|
2639
|
+
defaultContext,
|
|
2640
|
+
applyVendorHints,
|
|
2641
|
+
defaultPromptCompression,
|
|
2642
|
+
promptCompressionStats,
|
|
2643
|
+
defaultSampling,
|
|
2644
|
+
provider
|
|
2645
|
+
) : void 0;
|
|
2646
|
+
const handler = {
|
|
2647
|
+
get(target, prop, receiver) {
|
|
2648
|
+
if (prop === "chat" && proxiedChat) {
|
|
2649
|
+
return proxiedChat;
|
|
2650
|
+
}
|
|
2651
|
+
if (prop === "responses" && typeof target.responses !== "undefined") {
|
|
2652
|
+
return proxiedResponses ?? Reflect.get(target, prop, receiver);
|
|
2653
|
+
}
|
|
2654
|
+
if (prop === "toNextResponse") {
|
|
2655
|
+
return toNextResponse;
|
|
2656
|
+
}
|
|
2657
|
+
if (prop === "pipeToResponse") {
|
|
2658
|
+
return pipeToResponse;
|
|
2659
|
+
}
|
|
2660
|
+
if (prop === "promptCompression") {
|
|
2661
|
+
return promptCompressionStats;
|
|
2662
|
+
}
|
|
2663
|
+
if (prop === "unwrap") {
|
|
2664
|
+
return () => target;
|
|
2665
|
+
}
|
|
2666
|
+
return Reflect.get(target, prop, receiver);
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
return new Proxy(client, handler);
|
|
2670
|
+
}
|
|
2671
|
+
function createChatProxy(resource, usageTap, defaultContext, applyVendorHints, defaultPromptCompression, promptCompressionStats, defaultSampling, provider) {
|
|
2672
|
+
const completions = createChatCompletionsProxy(
|
|
2673
|
+
resource.completions,
|
|
2674
|
+
usageTap,
|
|
2675
|
+
defaultContext,
|
|
2676
|
+
applyVendorHints,
|
|
2677
|
+
defaultPromptCompression,
|
|
2678
|
+
promptCompressionStats,
|
|
2679
|
+
defaultSampling,
|
|
2680
|
+
provider
|
|
2681
|
+
);
|
|
2682
|
+
const handler = {
|
|
2683
|
+
get(target, prop, receiver) {
|
|
2684
|
+
if (prop === "completions") {
|
|
2685
|
+
return completions;
|
|
2686
|
+
}
|
|
2687
|
+
return Reflect.get(target, prop, receiver);
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
return new Proxy(resource, handler);
|
|
2691
|
+
}
|
|
2692
|
+
function createResponsesProxy(resource, usageTap, defaultContext, applyVendorHints, defaultPromptCompression, promptCompressionStats, defaultSampling, provider) {
|
|
2693
|
+
if (!resource || typeof resource !== "object") {
|
|
2694
|
+
return void 0;
|
|
2695
|
+
}
|
|
2696
|
+
if (!("create" in resource) || typeof resource.create !== "function") {
|
|
2697
|
+
return resource;
|
|
2698
|
+
}
|
|
2699
|
+
const originalCreate = resource.create.bind(resource);
|
|
2700
|
+
const wrappedCreate = (params, options) => {
|
|
2701
|
+
const {
|
|
2702
|
+
requestOptions,
|
|
2703
|
+
usageContext,
|
|
2704
|
+
withUsage,
|
|
2705
|
+
promptCompression
|
|
2706
|
+
} = splitUsageOptions(options);
|
|
2707
|
+
const beginRequest = responsesBeginRequest(
|
|
2708
|
+
resolveBeginRequest(defaultContext, usageContext),
|
|
2709
|
+
params
|
|
2710
|
+
);
|
|
2711
|
+
const wantsStream = isStreamingRequest(params);
|
|
2712
|
+
return usageTap.withUsage(beginRequest, async (ctx) => {
|
|
2713
|
+
const settle = wantsStream ? deferUsageFinalization(ctx) : void 0;
|
|
2714
|
+
const sampleStartedAt = Date.now();
|
|
2715
|
+
const sampleDecision = wantsStream ? Promise.resolve(false) : startMeteredOpenAISampleDecision({
|
|
2716
|
+
usageTap,
|
|
2717
|
+
sampling: defaultSampling,
|
|
2718
|
+
beginRequest,
|
|
2719
|
+
input: params
|
|
2720
|
+
});
|
|
2721
|
+
const hintedParams = applyVendorHints ? applyResponsesVendorHints(params, ctx.begin.data.vendorHints) : params;
|
|
2722
|
+
const finalParams = await compressResponsesParamsForCall({
|
|
2723
|
+
params: hintedParams,
|
|
2724
|
+
usageTap,
|
|
2725
|
+
ctx,
|
|
2726
|
+
defaultPromptCompression,
|
|
2727
|
+
callPromptCompression: promptCompression,
|
|
2728
|
+
stats: promptCompressionStats,
|
|
2729
|
+
withUsage,
|
|
2730
|
+
operation: "responses.create"
|
|
2731
|
+
});
|
|
2732
|
+
ctx.setUsage(openAIRequestExecutionMetadata(finalParams, provider));
|
|
2733
|
+
const request = attachCorrelationHeader(requestOptions, ctx.begin.correlationId);
|
|
2734
|
+
if (wantsStream) {
|
|
2735
|
+
const apiPromise2 = originalCreate(finalParams, request);
|
|
2736
|
+
const wrappedPromise2 = transformApiPromise(apiPromise2, (rawStream) => {
|
|
2737
|
+
ensureAsyncIterable(rawStream, "responses.create");
|
|
2738
|
+
const wrappedStream = wrapStreamForUsageTap(rawStream, async (termination) => {
|
|
2739
|
+
try {
|
|
2740
|
+
if (termination === "complete") {
|
|
2741
|
+
const usage = await extractUsageFromStream(rawStream, ctx.begin.data.vendorHints, provider);
|
|
2742
|
+
if (usage) {
|
|
2743
|
+
ctx.setUsage(usage);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
ctx.setError({
|
|
2748
|
+
code: "USAGE_FINALIZE_ERROR",
|
|
2749
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2750
|
+
});
|
|
2751
|
+
throw error;
|
|
2752
|
+
} finally {
|
|
2753
|
+
await settle?.();
|
|
2754
|
+
}
|
|
2755
|
+
}, ctx, (chunk) => {
|
|
2756
|
+
tryInferUsageFromStreamChunk(
|
|
2757
|
+
chunk,
|
|
2758
|
+
ctx.begin.data.vendorHints,
|
|
2759
|
+
ctx,
|
|
2760
|
+
provider
|
|
2761
|
+
);
|
|
2762
|
+
});
|
|
2763
|
+
return wrappedStream;
|
|
2764
|
+
});
|
|
2765
|
+
return wrappedPromise2;
|
|
2766
|
+
}
|
|
2767
|
+
const apiPromise = originalCreate(finalParams, request);
|
|
2768
|
+
const wrappedPromise = transformApiPromise(apiPromise, async (response) => {
|
|
2769
|
+
tryInferUsage(response, ctx.begin.data.vendorHints, void 0, ctx, provider);
|
|
2770
|
+
await captureMeteredOpenAISample({
|
|
2771
|
+
usageTap,
|
|
2772
|
+
sampling: defaultSampling,
|
|
2773
|
+
decision: sampleDecision,
|
|
2774
|
+
ctx,
|
|
2775
|
+
beginRequest,
|
|
2776
|
+
input: params,
|
|
2777
|
+
response,
|
|
2778
|
+
startedAt: sampleStartedAt
|
|
2779
|
+
});
|
|
2780
|
+
return response;
|
|
2781
|
+
}, async (error) => {
|
|
2782
|
+
await captureMeteredOpenAISample({
|
|
2783
|
+
usageTap,
|
|
2784
|
+
sampling: defaultSampling,
|
|
2785
|
+
decision: sampleDecision,
|
|
2786
|
+
ctx,
|
|
2787
|
+
beginRequest,
|
|
2788
|
+
input: params,
|
|
2789
|
+
error,
|
|
2790
|
+
startedAt: sampleStartedAt
|
|
2791
|
+
});
|
|
2792
|
+
throw error;
|
|
2793
|
+
});
|
|
2794
|
+
return wrappedPromise;
|
|
2795
|
+
}, withUsage);
|
|
2796
|
+
};
|
|
2797
|
+
const handler = {
|
|
2798
|
+
get(target, prop, receiver) {
|
|
2799
|
+
if (prop === "create") {
|
|
2800
|
+
return wrappedCreate;
|
|
2801
|
+
}
|
|
2802
|
+
return Reflect.get(target, prop, receiver);
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
return new Proxy(resource, handler);
|
|
2806
|
+
}
|
|
2807
|
+
function createChatCompletionsProxy(resource, usageTap, defaultContext, applyVendorHints, defaultPromptCompression, promptCompressionStats, defaultSampling, provider) {
|
|
2808
|
+
const originalCreate = resource.create.bind(resource);
|
|
2809
|
+
const streamCandidate = resource.stream;
|
|
2810
|
+
const originalStream = typeof streamCandidate === "function" ? streamCandidate.bind(resource) : void 0;
|
|
2811
|
+
const wrappedCreate = (params, options) => {
|
|
2812
|
+
const {
|
|
2813
|
+
requestOptions,
|
|
2814
|
+
usageContext,
|
|
2815
|
+
withUsage,
|
|
2816
|
+
promptCompression
|
|
2817
|
+
} = splitUsageOptions(options);
|
|
2818
|
+
const beginRequest = resolveBeginRequest(defaultContext, usageContext);
|
|
2819
|
+
const wantsStream = isStreamingRequest(params);
|
|
2820
|
+
return usageTap.withUsage(beginRequest, async (ctx) => {
|
|
2821
|
+
const settle = wantsStream ? deferUsageFinalization(ctx) : void 0;
|
|
2822
|
+
const sampleStartedAt = Date.now();
|
|
2823
|
+
const sampleDecision = wantsStream ? Promise.resolve(false) : startMeteredOpenAISampleDecision({
|
|
2824
|
+
usageTap,
|
|
2825
|
+
sampling: defaultSampling,
|
|
2826
|
+
beginRequest,
|
|
2827
|
+
input: params
|
|
2828
|
+
});
|
|
2829
|
+
const hintedParams = applyVendorHints ? applyChatVendorHints(params, ctx.begin.data.vendorHints) : params;
|
|
2830
|
+
const compressedParams = await compressChatParamsForCall({
|
|
2831
|
+
params: hintedParams,
|
|
2832
|
+
usageTap,
|
|
2833
|
+
ctx,
|
|
2834
|
+
defaultPromptCompression,
|
|
2835
|
+
callPromptCompression: promptCompression,
|
|
2836
|
+
stats: promptCompressionStats,
|
|
2837
|
+
withUsage,
|
|
2838
|
+
operation: "chat.completions.create"
|
|
2839
|
+
});
|
|
2840
|
+
const finalParams = wantsStream ? ensureOpenAIStreamUsage(compressedParams) : compressedParams;
|
|
2841
|
+
ctx.setUsage(openAIRequestExecutionMetadata(finalParams, provider));
|
|
2842
|
+
const request = attachCorrelationHeader(requestOptions, ctx.begin.correlationId);
|
|
2843
|
+
if (wantsStream) {
|
|
2844
|
+
const apiPromise2 = originalCreate(finalParams, request);
|
|
2845
|
+
const wrappedPromise2 = transformApiPromise(apiPromise2, (rawStream) => {
|
|
2846
|
+
ensureAsyncIterable(rawStream, "chat.completions.create");
|
|
2847
|
+
const wrappedStream2 = wrapStreamForUsageTap(rawStream, async (termination) => {
|
|
33
2848
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
2849
|
+
if (termination === "complete") {
|
|
2850
|
+
const usage = await extractUsageFromStream(rawStream, ctx.begin.data.vendorHints, provider);
|
|
2851
|
+
if (usage) {
|
|
2852
|
+
ctx.setUsage(usage);
|
|
2853
|
+
}
|
|
37
2854
|
}
|
|
38
2855
|
} catch (error) {
|
|
39
2856
|
ctx.setError({
|
|
@@ -41,34 +2858,967 @@ function createOpenAIAdapter(init) {
|
|
|
41
2858
|
message: error instanceof Error ? error.message : String(error)
|
|
42
2859
|
});
|
|
43
2860
|
throw error;
|
|
2861
|
+
} finally {
|
|
2862
|
+
await settle?.();
|
|
44
2863
|
}
|
|
45
|
-
}, ctx)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
2864
|
+
}, ctx, (chunk) => {
|
|
2865
|
+
tryInferUsageFromStreamChunk(
|
|
2866
|
+
chunk,
|
|
2867
|
+
ctx.begin.data.vendorHints,
|
|
2868
|
+
ctx,
|
|
2869
|
+
provider
|
|
2870
|
+
);
|
|
2871
|
+
});
|
|
2872
|
+
return wrappedStream2;
|
|
2873
|
+
});
|
|
2874
|
+
return wrappedPromise2;
|
|
2875
|
+
}
|
|
2876
|
+
const apiPromise = originalCreate(finalParams, request);
|
|
2877
|
+
const wrappedPromise = transformApiPromise(apiPromise, async (response) => {
|
|
2878
|
+
tryInferUsage(response, ctx.begin.data.vendorHints, void 0, ctx, provider);
|
|
2879
|
+
await captureMeteredOpenAISample({
|
|
2880
|
+
usageTap,
|
|
2881
|
+
sampling: defaultSampling,
|
|
2882
|
+
decision: sampleDecision,
|
|
2883
|
+
ctx,
|
|
2884
|
+
beginRequest,
|
|
2885
|
+
input: params,
|
|
2886
|
+
response,
|
|
2887
|
+
startedAt: sampleStartedAt
|
|
2888
|
+
});
|
|
2889
|
+
return response;
|
|
2890
|
+
}, async (error) => {
|
|
2891
|
+
await captureMeteredOpenAISample({
|
|
2892
|
+
usageTap,
|
|
2893
|
+
sampling: defaultSampling,
|
|
2894
|
+
decision: sampleDecision,
|
|
2895
|
+
ctx,
|
|
2896
|
+
beginRequest,
|
|
2897
|
+
input: params,
|
|
2898
|
+
error,
|
|
2899
|
+
startedAt: sampleStartedAt
|
|
2900
|
+
});
|
|
2901
|
+
throw error;
|
|
2902
|
+
});
|
|
2903
|
+
return wrappedPromise;
|
|
2904
|
+
}, withUsage);
|
|
2905
|
+
};
|
|
2906
|
+
const wrappedStream = originalStream ? (params, options) => {
|
|
2907
|
+
const {
|
|
2908
|
+
requestOptions,
|
|
2909
|
+
usageContext,
|
|
2910
|
+
withUsage,
|
|
2911
|
+
promptCompression
|
|
2912
|
+
} = splitUsageOptions(options);
|
|
2913
|
+
const beginRequest = resolveBeginRequest(defaultContext, usageContext);
|
|
2914
|
+
return usageTap.withUsage(beginRequest, async (ctx) => {
|
|
2915
|
+
const settle = deferUsageFinalization(ctx);
|
|
2916
|
+
const hintedParams = applyVendorHints ? applyChatVendorHints(params, ctx.begin.data.vendorHints) : params;
|
|
2917
|
+
const compressedParams = await compressChatParamsForCall({
|
|
2918
|
+
params: hintedParams,
|
|
2919
|
+
usageTap,
|
|
2920
|
+
ctx,
|
|
2921
|
+
defaultPromptCompression,
|
|
2922
|
+
callPromptCompression: promptCompression,
|
|
2923
|
+
stats: promptCompressionStats,
|
|
2924
|
+
withUsage,
|
|
2925
|
+
operation: "chat.completions.stream"
|
|
2926
|
+
});
|
|
2927
|
+
const finalParams = ensureOpenAIStreamUsage(compressedParams);
|
|
2928
|
+
ctx.setUsage(openAIRequestExecutionMetadata(finalParams, provider));
|
|
2929
|
+
const request = attachCorrelationHeader(requestOptions, ctx.begin.correlationId);
|
|
2930
|
+
const apiPromise = originalStream(finalParams, request);
|
|
2931
|
+
const wrappedPromise = transformApiPromise(apiPromise, (rawStream) => {
|
|
2932
|
+
ensureAsyncIterable(rawStream, "chat.completions.stream");
|
|
2933
|
+
const wrappedStreamInner = wrapStreamForUsageTap(rawStream, async (termination) => {
|
|
2934
|
+
try {
|
|
2935
|
+
if (termination === "complete") {
|
|
2936
|
+
const usage = await extractUsageFromStream(
|
|
2937
|
+
rawStream,
|
|
2938
|
+
ctx.begin.data.vendorHints,
|
|
2939
|
+
provider
|
|
2940
|
+
);
|
|
2941
|
+
if (usage) {
|
|
2942
|
+
ctx.setUsage(usage);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
ctx.setError({
|
|
2947
|
+
code: "USAGE_FINALIZE_ERROR",
|
|
2948
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2949
|
+
});
|
|
2950
|
+
throw error;
|
|
2951
|
+
} finally {
|
|
2952
|
+
await settle();
|
|
2953
|
+
}
|
|
2954
|
+
}, ctx, (chunk) => {
|
|
2955
|
+
tryInferUsageFromStreamChunk(
|
|
2956
|
+
chunk,
|
|
2957
|
+
ctx.begin.data.vendorHints,
|
|
2958
|
+
ctx,
|
|
2959
|
+
provider
|
|
2960
|
+
);
|
|
2961
|
+
});
|
|
2962
|
+
return wrappedStreamInner;
|
|
2963
|
+
});
|
|
2964
|
+
return wrappedPromise;
|
|
2965
|
+
}, withUsage);
|
|
2966
|
+
} : void 0;
|
|
2967
|
+
const handler = {
|
|
2968
|
+
get(target, prop, receiver) {
|
|
2969
|
+
if (prop === "create") {
|
|
2970
|
+
return wrappedCreate;
|
|
2971
|
+
}
|
|
2972
|
+
if (prop === "stream" && wrappedStream) {
|
|
2973
|
+
return wrappedStream;
|
|
2974
|
+
}
|
|
2975
|
+
return Reflect.get(target, prop, receiver);
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
return new Proxy(resource, handler);
|
|
2979
|
+
}
|
|
2980
|
+
async function compressChatParamsForCall(args) {
|
|
2981
|
+
const compression = resolveEffectivePromptCompressionOptions(
|
|
2982
|
+
args.defaultPromptCompression,
|
|
2983
|
+
args.callPromptCompression
|
|
2984
|
+
);
|
|
2985
|
+
if (!compression) {
|
|
2986
|
+
return args.params;
|
|
2987
|
+
}
|
|
2988
|
+
const outcome = await compressChatParams(
|
|
2989
|
+
args.params,
|
|
2990
|
+
args.usageTap,
|
|
2991
|
+
compression,
|
|
2992
|
+
args.withUsage?.signal
|
|
2993
|
+
);
|
|
2994
|
+
await recordCompressionOutcome({
|
|
2995
|
+
outcome,
|
|
2996
|
+
compression,
|
|
2997
|
+
usageTap: args.usageTap,
|
|
2998
|
+
ctx: args.ctx,
|
|
2999
|
+
stats: args.stats,
|
|
3000
|
+
withUsage: args.withUsage,
|
|
3001
|
+
operation: args.operation
|
|
3002
|
+
});
|
|
3003
|
+
return outcome.params;
|
|
3004
|
+
}
|
|
3005
|
+
async function compressResponsesParamsForCall(args) {
|
|
3006
|
+
const compression = resolveEffectivePromptCompressionOptions(
|
|
3007
|
+
args.defaultPromptCompression,
|
|
3008
|
+
args.callPromptCompression
|
|
3009
|
+
);
|
|
3010
|
+
if (!compression) {
|
|
3011
|
+
return args.params;
|
|
3012
|
+
}
|
|
3013
|
+
const outcome = await compressResponsesParams(
|
|
3014
|
+
args.params,
|
|
3015
|
+
args.usageTap,
|
|
3016
|
+
compression,
|
|
3017
|
+
args.withUsage?.signal
|
|
3018
|
+
);
|
|
3019
|
+
await recordCompressionOutcome({
|
|
3020
|
+
outcome,
|
|
3021
|
+
compression,
|
|
3022
|
+
usageTap: args.usageTap,
|
|
3023
|
+
ctx: args.ctx,
|
|
3024
|
+
stats: args.stats,
|
|
3025
|
+
withUsage: args.withUsage,
|
|
3026
|
+
operation: args.operation
|
|
3027
|
+
});
|
|
3028
|
+
return outcome.params;
|
|
3029
|
+
}
|
|
3030
|
+
async function recordCompressionOutcome(args) {
|
|
3031
|
+
const telemetry = buildPromptCompressionTelemetry(args.outcome.segments);
|
|
3032
|
+
if (!telemetry) {
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
const turn = {
|
|
3036
|
+
...telemetry,
|
|
3037
|
+
callId: args.ctx.begin.data.callId,
|
|
3038
|
+
operation: args.operation,
|
|
3039
|
+
messagesCompressed: args.outcome.segments.length,
|
|
3040
|
+
timestamp: Date.now()
|
|
3041
|
+
};
|
|
3042
|
+
args.stats._record(turn);
|
|
3043
|
+
try {
|
|
3044
|
+
await args.usageTap.recordPromptCompression(
|
|
3045
|
+
{
|
|
3046
|
+
callId: args.ctx.begin.data.callId,
|
|
3047
|
+
promptCompression: telemetry
|
|
3048
|
+
},
|
|
3049
|
+
promptCompressionRequestOptions(args.withUsage, args.ctx.begin.correlationId)
|
|
3050
|
+
);
|
|
3051
|
+
} catch (error) {
|
|
3052
|
+
args.stats._recordFailure({
|
|
3053
|
+
callId: args.ctx.begin.data.callId,
|
|
3054
|
+
operation: args.operation,
|
|
3055
|
+
stage: "telemetry",
|
|
3056
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3057
|
+
timestamp: Date.now()
|
|
3058
|
+
});
|
|
3059
|
+
if (args.compression.failOpen === false) {
|
|
3060
|
+
throw error;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
async function compressChatParams(params, usageTap, compression, signal) {
|
|
3065
|
+
if (!params || typeof params !== "object") {
|
|
3066
|
+
return { params, segments: [] };
|
|
3067
|
+
}
|
|
3068
|
+
const source = cloneRecord(params);
|
|
3069
|
+
const messages = Array.isArray(source.messages) ? source.messages : void 0;
|
|
3070
|
+
if (!messages) {
|
|
3071
|
+
return { params, segments: [] };
|
|
3072
|
+
}
|
|
3073
|
+
if (isBelowMinContextTokens(
|
|
3074
|
+
{ messages, tools: source.tools },
|
|
3075
|
+
compression.minContextTokens
|
|
3076
|
+
)) {
|
|
3077
|
+
return { params, segments: [] };
|
|
3078
|
+
}
|
|
3079
|
+
if (shouldUseUsageTapMessageEndpoint(compression)) {
|
|
3080
|
+
const result = await usageTap.compressPromptMessages(source, {
|
|
3081
|
+
provider: "usagetap",
|
|
3082
|
+
failOpen: compression.failOpen,
|
|
3083
|
+
mode: compression.mode,
|
|
3084
|
+
latencyBudgetMs: compression.latencyBudgetMs,
|
|
3085
|
+
compactEmptyUserMessages: compression.compactEmptyUserMessages,
|
|
3086
|
+
compactDuplicateUserTextParts: compression.compactDuplicateUserTextParts,
|
|
3087
|
+
aggressiveness: resolveMessageEndpointAggressiveness(compression),
|
|
3088
|
+
signal
|
|
3089
|
+
});
|
|
3090
|
+
return {
|
|
3091
|
+
params: result.compressedInput,
|
|
3092
|
+
segments: [{ role: "user", result }]
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
const messageResults = await Promise.all(
|
|
3096
|
+
messages.map(
|
|
3097
|
+
(message) => compressOpenAIMessage(message, usageTap, compression, signal)
|
|
3098
|
+
)
|
|
3099
|
+
);
|
|
3100
|
+
return {
|
|
3101
|
+
params: {
|
|
3102
|
+
...source,
|
|
3103
|
+
messages: messageResults.map((result) => result.value)
|
|
3104
|
+
},
|
|
3105
|
+
segments: messageResults.flatMap((result) => result.segments)
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
async function compressResponsesParams(params, usageTap, compression, signal) {
|
|
3109
|
+
if (!params || typeof params !== "object") {
|
|
3110
|
+
return { params, segments: [] };
|
|
3111
|
+
}
|
|
3112
|
+
const source = cloneRecord(params);
|
|
3113
|
+
const segments = [];
|
|
3114
|
+
if (isBelowMinContextTokens(
|
|
3115
|
+
{
|
|
3116
|
+
instructions: source.instructions,
|
|
3117
|
+
input: source.input,
|
|
3118
|
+
tools: source.tools
|
|
3119
|
+
},
|
|
3120
|
+
compression.minContextTokens
|
|
3121
|
+
)) {
|
|
3122
|
+
return { params, segments };
|
|
3123
|
+
}
|
|
3124
|
+
if (typeof source.instructions === "string") {
|
|
3125
|
+
const compressed = await compressTextForRole(
|
|
3126
|
+
source.instructions,
|
|
3127
|
+
"system",
|
|
3128
|
+
usageTap,
|
|
3129
|
+
compression,
|
|
3130
|
+
signal
|
|
3131
|
+
);
|
|
3132
|
+
if (compressed) {
|
|
3133
|
+
source.instructions = compressed.text;
|
|
3134
|
+
segments.push(compressed.segment);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
if (typeof source.input === "string") {
|
|
3138
|
+
const compressed = await compressTextForRole(
|
|
3139
|
+
source.input,
|
|
3140
|
+
"user",
|
|
3141
|
+
usageTap,
|
|
3142
|
+
compression,
|
|
3143
|
+
signal
|
|
3144
|
+
);
|
|
3145
|
+
if (compressed) {
|
|
3146
|
+
source.input = compressed.text;
|
|
3147
|
+
segments.push(compressed.segment);
|
|
3148
|
+
}
|
|
3149
|
+
} else if (Array.isArray(source.input)) {
|
|
3150
|
+
const inputResults = await Promise.all(
|
|
3151
|
+
source.input.map(
|
|
3152
|
+
(item) => compressResponsesInputItem(item, usageTap, compression, signal)
|
|
3153
|
+
)
|
|
3154
|
+
);
|
|
3155
|
+
source.input = inputResults.map((result) => result.value);
|
|
3156
|
+
segments.push(...inputResults.flatMap((result) => result.segments));
|
|
3157
|
+
}
|
|
3158
|
+
return {
|
|
3159
|
+
params: source,
|
|
3160
|
+
segments
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
async function compressOpenAIMessage(message, usageTap, compression, signal) {
|
|
3164
|
+
if (!isObjectRecord(message)) {
|
|
3165
|
+
return { value: message, segments: [] };
|
|
3166
|
+
}
|
|
3167
|
+
const role = mapOpenAIRole(message.role);
|
|
3168
|
+
if (!role) {
|
|
3169
|
+
return { value: message, segments: [] };
|
|
3170
|
+
}
|
|
3171
|
+
const content = message.content;
|
|
3172
|
+
if (typeof content === "string") {
|
|
3173
|
+
const compressed = await compressTextForRole(
|
|
3174
|
+
content,
|
|
3175
|
+
role,
|
|
3176
|
+
usageTap,
|
|
3177
|
+
compression,
|
|
3178
|
+
signal
|
|
3179
|
+
);
|
|
3180
|
+
if (!compressed) {
|
|
3181
|
+
return { value: message, segments: [] };
|
|
3182
|
+
}
|
|
3183
|
+
return {
|
|
3184
|
+
value: { ...message, content: compressed.text },
|
|
3185
|
+
segments: [compressed.segment]
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
if (Array.isArray(content)) {
|
|
3189
|
+
const blockResults = await Promise.all(
|
|
3190
|
+
content.map(
|
|
3191
|
+
(block) => compressOpenAITextBlock(block, role, usageTap, compression, signal)
|
|
3192
|
+
)
|
|
3193
|
+
);
|
|
3194
|
+
const segments = blockResults.flatMap(
|
|
3195
|
+
(result) => result.segment ? [result.segment] : []
|
|
3196
|
+
);
|
|
3197
|
+
return {
|
|
3198
|
+
value: segments.length ? { ...message, content: blockResults.map((result) => result.value) } : message,
|
|
3199
|
+
segments
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
return { value: message, segments: [] };
|
|
3203
|
+
}
|
|
3204
|
+
async function compressOpenAITextBlock(block, role, usageTap, compression, signal) {
|
|
3205
|
+
if (!isObjectRecord(block) || block.type !== "text" || typeof block.text !== "string") {
|
|
3206
|
+
return { value: block };
|
|
3207
|
+
}
|
|
3208
|
+
const compressed = await compressTextForRole(
|
|
3209
|
+
block.text,
|
|
3210
|
+
role,
|
|
3211
|
+
usageTap,
|
|
3212
|
+
compression,
|
|
3213
|
+
signal
|
|
3214
|
+
);
|
|
3215
|
+
if (!compressed) {
|
|
3216
|
+
return { value: block };
|
|
3217
|
+
}
|
|
3218
|
+
return {
|
|
3219
|
+
value: { ...block, text: compressed.text },
|
|
3220
|
+
segment: compressed.segment
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
async function compressResponsesInputItem(item, usageTap, compression, signal) {
|
|
3224
|
+
if (!isObjectRecord(item)) {
|
|
3225
|
+
return { value: item, segments: [] };
|
|
3226
|
+
}
|
|
3227
|
+
const specialToolRole = mapResponsesItemTypeToRole(item.type);
|
|
3228
|
+
const role = specialToolRole ?? mapOpenAIRole(item.role);
|
|
3229
|
+
const segments = [];
|
|
3230
|
+
let next = item;
|
|
3231
|
+
if (role && typeof item.content === "string") {
|
|
3232
|
+
const compressed = await compressTextForRole(
|
|
3233
|
+
item.content,
|
|
3234
|
+
role,
|
|
3235
|
+
usageTap,
|
|
3236
|
+
compression,
|
|
3237
|
+
signal
|
|
3238
|
+
);
|
|
3239
|
+
if (compressed) {
|
|
3240
|
+
next = { ...next, content: compressed.text };
|
|
3241
|
+
segments.push(compressed.segment);
|
|
3242
|
+
}
|
|
3243
|
+
} else if (role && Array.isArray(item.content)) {
|
|
3244
|
+
const contentResults = await Promise.all(
|
|
3245
|
+
item.content.map(
|
|
3246
|
+
(block) => compressResponsesContentBlock(block, role, usageTap, compression, signal)
|
|
3247
|
+
)
|
|
3248
|
+
);
|
|
3249
|
+
segments.push(
|
|
3250
|
+
...contentResults.flatMap(
|
|
3251
|
+
(result) => result.segment ? [result.segment] : []
|
|
3252
|
+
)
|
|
3253
|
+
);
|
|
3254
|
+
if (segments.length) {
|
|
3255
|
+
next = {
|
|
3256
|
+
...next,
|
|
3257
|
+
content: contentResults.map((result) => result.value)
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (specialToolRole && typeof item.output === "string") {
|
|
3262
|
+
const compressed = await compressTextForRole(
|
|
3263
|
+
item.output,
|
|
3264
|
+
specialToolRole,
|
|
3265
|
+
usageTap,
|
|
3266
|
+
compression,
|
|
3267
|
+
signal
|
|
3268
|
+
);
|
|
3269
|
+
if (compressed) {
|
|
3270
|
+
next = { ...next, output: compressed.text };
|
|
3271
|
+
segments.push(compressed.segment);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
return { value: next, segments };
|
|
3275
|
+
}
|
|
3276
|
+
async function compressResponsesContentBlock(block, role, usageTap, compression, signal) {
|
|
3277
|
+
if (!isObjectRecord(block)) {
|
|
3278
|
+
return { value: block };
|
|
3279
|
+
}
|
|
3280
|
+
if ((block.type === "input_text" || block.type === "text") && typeof block.text === "string") {
|
|
3281
|
+
const compressed = await compressTextForRole(
|
|
3282
|
+
block.text,
|
|
3283
|
+
role,
|
|
3284
|
+
usageTap,
|
|
3285
|
+
compression,
|
|
3286
|
+
signal
|
|
3287
|
+
);
|
|
3288
|
+
if (compressed) {
|
|
3289
|
+
return {
|
|
3290
|
+
value: { ...block, text: compressed.text },
|
|
3291
|
+
segment: compressed.segment
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
if (role === "tool" && typeof block.output === "string") {
|
|
3296
|
+
const compressed = await compressTextForRole(
|
|
3297
|
+
block.output,
|
|
3298
|
+
role,
|
|
3299
|
+
usageTap,
|
|
3300
|
+
compression,
|
|
3301
|
+
signal
|
|
3302
|
+
);
|
|
3303
|
+
if (compressed) {
|
|
3304
|
+
return {
|
|
3305
|
+
value: { ...block, output: compressed.text },
|
|
3306
|
+
segment: compressed.segment
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
return { value: block };
|
|
3311
|
+
}
|
|
3312
|
+
async function compressTextForRole(text, role, usageTap, compression, signal) {
|
|
3313
|
+
if (!text.trim()) {
|
|
3314
|
+
return void 0;
|
|
3315
|
+
}
|
|
3316
|
+
const roleOptions = resolveRoleCompressionOptions(compression, role);
|
|
3317
|
+
if (!roleOptions) {
|
|
3318
|
+
return void 0;
|
|
3319
|
+
}
|
|
3320
|
+
const estimatedTokens = estimatePromptTokens(text);
|
|
3321
|
+
if (typeof roleOptions.minTokens === "number" && estimatedTokens < roleOptions.minTokens) {
|
|
3322
|
+
return void 0;
|
|
3323
|
+
}
|
|
3324
|
+
const result = await usageTap.compressPromptInput(text, {
|
|
3325
|
+
provider: roleOptions.provider,
|
|
3326
|
+
failOpen: roleOptions.failOpen,
|
|
3327
|
+
tokenCompanyModel: roleOptions.tokenCompanyModel,
|
|
3328
|
+
aggressiveness: roleOptions.aggressiveness,
|
|
3329
|
+
tokenCompanyAggressiveness: roleOptions.tokenCompanyAggressiveness,
|
|
3330
|
+
tokenCompanyAppId: roleOptions.tokenCompanyAppId,
|
|
3331
|
+
usageTapCompressionModel: roleOptions.usageTapCompressionModel,
|
|
3332
|
+
usageTapCompressionAggressiveness: roleOptions.usageTapCompressionAggressiveness,
|
|
3333
|
+
signal
|
|
3334
|
+
});
|
|
3335
|
+
const compressedText = typeof result.compressedInput === "string" ? result.compressedInput : String(result.compressedInput);
|
|
3336
|
+
return {
|
|
3337
|
+
text: compressedText,
|
|
3338
|
+
segment: { role, result: { ...result, compressedInput: compressedText } }
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
function normalizePromptCompressionOptions(options) {
|
|
3342
|
+
if (!options) {
|
|
3343
|
+
return void 0;
|
|
3344
|
+
}
|
|
3345
|
+
if (options === true) {
|
|
3346
|
+
return {};
|
|
3347
|
+
}
|
|
3348
|
+
if (options.enabled === false) {
|
|
3349
|
+
return void 0;
|
|
3350
|
+
}
|
|
3351
|
+
return options;
|
|
3352
|
+
}
|
|
3353
|
+
function isBelowMinContextTokens(context, minContextTokens) {
|
|
3354
|
+
return typeof minContextTokens === "number" && estimatePromptTokens(context) < Math.max(0, minContextTokens);
|
|
3355
|
+
}
|
|
3356
|
+
function resolveEffectivePromptCompressionOptions(defaults, override) {
|
|
3357
|
+
if (override === false) {
|
|
3358
|
+
return void 0;
|
|
3359
|
+
}
|
|
3360
|
+
if (override === void 0) {
|
|
3361
|
+
return defaults;
|
|
3362
|
+
}
|
|
3363
|
+
if (override === true) {
|
|
3364
|
+
return defaults ?? {};
|
|
3365
|
+
}
|
|
3366
|
+
const merged = {
|
|
3367
|
+
...defaults ?? {},
|
|
3368
|
+
...override,
|
|
3369
|
+
roles: override.roles ?? defaults?.roles
|
|
3370
|
+
};
|
|
3371
|
+
return normalizePromptCompressionOptions(merged);
|
|
3372
|
+
}
|
|
3373
|
+
function resolveRoleCompressionOptions(compression, role) {
|
|
3374
|
+
const hasExplicitRoles = compression.roles !== void 0;
|
|
3375
|
+
const setting = compression.roles?.[role];
|
|
3376
|
+
if (hasExplicitRoles && setting === void 0) {
|
|
3377
|
+
return void 0;
|
|
3378
|
+
}
|
|
3379
|
+
if (!hasExplicitRoles && role === "assistant") {
|
|
3380
|
+
return void 0;
|
|
3381
|
+
}
|
|
3382
|
+
if (setting === false) {
|
|
3383
|
+
return void 0;
|
|
3384
|
+
}
|
|
3385
|
+
const roleOptions = typeof setting === "object" ? setting : void 0;
|
|
3386
|
+
if (roleOptions?.enabled === false) {
|
|
3387
|
+
return void 0;
|
|
3388
|
+
}
|
|
3389
|
+
return {
|
|
3390
|
+
provider: roleOptions?.provider ?? compression.provider,
|
|
3391
|
+
minTokens: roleOptions?.minTokens ?? compression.minTokens,
|
|
3392
|
+
failOpen: compression.failOpen,
|
|
3393
|
+
tokenCompanyModel: compression.tokenCompanyModel,
|
|
3394
|
+
aggressiveness: roleOptions?.aggressiveness ?? resolveAggressiveness(compression, role),
|
|
3395
|
+
tokenCompanyAggressiveness: roleOptions?.tokenCompanyAggressiveness ?? resolveTokenCompanyAggressiveness(compression, role),
|
|
3396
|
+
tokenCompanyAppId: compression.tokenCompanyAppId,
|
|
3397
|
+
usageTapCompressionModel: compression.usageTapCompressionModel,
|
|
3398
|
+
usageTapCompressionAggressiveness: roleOptions?.usageTapCompressionAggressiveness ?? resolveUsageTapCompressionAggressiveness(compression, role)
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
function resolveAggressiveness(compression, role) {
|
|
3402
|
+
if (typeof compression.aggressiveness === "number") {
|
|
3403
|
+
return compression.aggressiveness;
|
|
3404
|
+
}
|
|
3405
|
+
return compression.aggressiveness?.[role];
|
|
3406
|
+
}
|
|
3407
|
+
function resolveTokenCompanyAggressiveness(compression, role) {
|
|
3408
|
+
if (typeof compression.tokenCompanyAggressiveness === "number") {
|
|
3409
|
+
return compression.tokenCompanyAggressiveness;
|
|
3410
|
+
}
|
|
3411
|
+
return compression.tokenCompanyAggressiveness?.[role];
|
|
3412
|
+
}
|
|
3413
|
+
function resolveUsageTapCompressionAggressiveness(compression, role) {
|
|
3414
|
+
if (typeof compression.usageTapCompressionAggressiveness === "number") {
|
|
3415
|
+
return compression.usageTapCompressionAggressiveness;
|
|
3416
|
+
}
|
|
3417
|
+
return compression.usageTapCompressionAggressiveness?.[role];
|
|
3418
|
+
}
|
|
3419
|
+
function shouldUseUsageTapMessageEndpoint(compression) {
|
|
3420
|
+
if (compression.provider !== "usagetap") {
|
|
3421
|
+
return false;
|
|
3422
|
+
}
|
|
3423
|
+
return Object.values(compression.roles ?? {}).every((setting) => {
|
|
3424
|
+
if (typeof setting !== "object" || setting === null) {
|
|
3425
|
+
return true;
|
|
3426
|
+
}
|
|
3427
|
+
return setting.provider === void 0 || setting.provider === "usagetap";
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
function resolveMessageEndpointAggressiveness(compression) {
|
|
3431
|
+
const base = compression.aggressiveness ?? compression.usageTapCompressionAggressiveness ?? compression.tokenCompanyAggressiveness;
|
|
3432
|
+
if (typeof base === "number" || base === void 0) {
|
|
3433
|
+
return hasExplicitEnabledRoles(compression) ? buildRoleAggressiveness(compression, base) : base;
|
|
3434
|
+
}
|
|
3435
|
+
return buildRoleAggressiveness(compression, void 0, base);
|
|
3436
|
+
}
|
|
3437
|
+
function hasExplicitEnabledRoles(compression) {
|
|
3438
|
+
return Object.values(compression.roles ?? {}).some((setting) => setting !== false);
|
|
3439
|
+
}
|
|
3440
|
+
function buildRoleAggressiveness(compression, fallback, base = {}) {
|
|
3441
|
+
const roles = ["system", "user", "tool", "assistant"];
|
|
3442
|
+
const result = {};
|
|
3443
|
+
for (const role of roles) {
|
|
3444
|
+
const roleOptions = resolveRoleCompressionOptions(compression, role);
|
|
3445
|
+
if (!roleOptions) {
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
const roleAggressiveness = roleOptions.aggressiveness ?? roleOptions.usageTapCompressionAggressiveness ?? roleOptions.tokenCompanyAggressiveness ?? base[role] ?? fallback;
|
|
3449
|
+
if (roleAggressiveness !== void 0) {
|
|
3450
|
+
result[role] = roleAggressiveness;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
return result;
|
|
3454
|
+
}
|
|
3455
|
+
function buildPromptCompressionTelemetry(segments) {
|
|
3456
|
+
if (!segments.length) {
|
|
3457
|
+
return void 0;
|
|
3458
|
+
}
|
|
3459
|
+
const originalCharacters = segments.reduce(
|
|
3460
|
+
(sum, segment) => sum + segment.result.originalCharacters,
|
|
3461
|
+
0
|
|
3462
|
+
);
|
|
3463
|
+
const compressedCharacters = segments.reduce(
|
|
3464
|
+
(sum, segment) => sum + segment.result.compressedCharacters,
|
|
3465
|
+
0
|
|
3466
|
+
);
|
|
3467
|
+
const originalTokens = segments.reduce(
|
|
3468
|
+
(sum, segment) => sum + segment.result.originalTokens,
|
|
3469
|
+
0
|
|
3470
|
+
);
|
|
3471
|
+
const compressedTokens = segments.reduce(
|
|
3472
|
+
(sum, segment) => sum + segment.result.compressedTokens,
|
|
3473
|
+
0
|
|
3474
|
+
);
|
|
3475
|
+
const savedCharacters = Math.max(0, originalCharacters - compressedCharacters);
|
|
3476
|
+
const savedTokens = Math.max(0, originalTokens - compressedTokens);
|
|
3477
|
+
const providers = dedupeStrings2(segments.map((segment) => segment.result.provider));
|
|
3478
|
+
const roles = dedupeStrings2(segments.map((segment) => `role:${segment.role}`));
|
|
3479
|
+
const techniques = dedupeStrings2([
|
|
3480
|
+
"openai-wrapper",
|
|
3481
|
+
...roles,
|
|
3482
|
+
...segments.flatMap((segment) => segment.result.techniques),
|
|
3483
|
+
...providers.length > 1 ? ["mixed-providers"] : []
|
|
3484
|
+
]);
|
|
3485
|
+
return {
|
|
3486
|
+
provider: segments[0]?.result.provider ?? "heuristic",
|
|
3487
|
+
originalCharacters,
|
|
3488
|
+
compressedCharacters,
|
|
3489
|
+
savedCharacters,
|
|
3490
|
+
originalTokens,
|
|
3491
|
+
compressedTokens,
|
|
3492
|
+
savedTokens,
|
|
3493
|
+
tokenSavingsRatio: originalTokens > 0 ? savedTokens / originalTokens : 0,
|
|
3494
|
+
savingsRatio: originalCharacters > 0 ? savedCharacters / originalCharacters : 0,
|
|
3495
|
+
techniques
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
function promptCompressionRequestOptions(withUsage, correlationId) {
|
|
3499
|
+
return {
|
|
3500
|
+
signal: withUsage?.signal,
|
|
3501
|
+
headers: withUsage?.headers,
|
|
3502
|
+
retries: withUsage?.retries,
|
|
3503
|
+
correlationId
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
function mapOpenAIRole(role) {
|
|
3507
|
+
if (role === "system" || role === "developer") {
|
|
3508
|
+
return "system";
|
|
3509
|
+
}
|
|
3510
|
+
if (role === "user") {
|
|
3511
|
+
return "user";
|
|
3512
|
+
}
|
|
3513
|
+
if (role === "tool" || role === "function") {
|
|
3514
|
+
return "tool";
|
|
3515
|
+
}
|
|
3516
|
+
if (role === "assistant") {
|
|
3517
|
+
return "assistant";
|
|
3518
|
+
}
|
|
3519
|
+
return void 0;
|
|
3520
|
+
}
|
|
3521
|
+
function mapResponsesItemTypeToRole(type) {
|
|
3522
|
+
if (type === "function_call_output" || type === "tool_result" || type === "computer_call_output") {
|
|
3523
|
+
return "tool";
|
|
3524
|
+
}
|
|
3525
|
+
return void 0;
|
|
3526
|
+
}
|
|
3527
|
+
function splitUsageOptions(options) {
|
|
3528
|
+
if (!options || typeof options !== "object") {
|
|
3529
|
+
return {};
|
|
3530
|
+
}
|
|
3531
|
+
const { usageTap, withUsage, promptCompression, ...rest } = options;
|
|
3532
|
+
const requestOptions = Object.keys(rest).length ? cloneRequestOptions(rest) : void 0;
|
|
3533
|
+
return {
|
|
3534
|
+
requestOptions,
|
|
3535
|
+
usageContext: usageTap,
|
|
3536
|
+
withUsage,
|
|
3537
|
+
promptCompression
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
function resolveBeginRequest(defaults, override) {
|
|
3541
|
+
const base = defaults ?? {};
|
|
3542
|
+
const current = override ?? {};
|
|
3543
|
+
const customerId = current.customerId ?? base.customerId;
|
|
3544
|
+
if (!customerId) {
|
|
3545
|
+
throw new UsageTapError(
|
|
3546
|
+
"USAGETAP_BAD_REQUEST",
|
|
3547
|
+
"wrapOpenAI requires usageTap.customerId (provide defaultContext or options.usageTap)"
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
const tags = mergeTags(base.tags, current.tags);
|
|
3551
|
+
const begin = { customerId };
|
|
3552
|
+
const requested = current.requested ?? base.requested;
|
|
3553
|
+
if (requested) begin.requested = requested;
|
|
3554
|
+
const feature = current.feature ?? base.feature;
|
|
3555
|
+
if (feature) begin.feature = feature;
|
|
3556
|
+
const runId = current.runId ?? base.runId;
|
|
3557
|
+
if (runId) begin.runId = runId;
|
|
3558
|
+
const idempotency = current.idempotency ?? base.idempotency;
|
|
3559
|
+
if (idempotency) begin.idempotency = idempotency;
|
|
3560
|
+
const customerName = current.customerName ?? base.customerName;
|
|
3561
|
+
if (customerName) begin.customerName = customerName;
|
|
3562
|
+
const customerEmail = current.customerEmail ?? base.customerEmail;
|
|
3563
|
+
if (customerEmail) begin.customerEmail = customerEmail;
|
|
3564
|
+
const customerUserId = current.customerUserId ?? base.customerUserId;
|
|
3565
|
+
if (customerUserId) begin.customerUserId = customerUserId;
|
|
3566
|
+
const customerUserName = current.customerUserName ?? base.customerUserName;
|
|
3567
|
+
if (customerUserName) begin.customerUserName = customerUserName;
|
|
3568
|
+
const customerUserEmail = current.customerUserEmail ?? base.customerUserEmail;
|
|
3569
|
+
if (customerUserEmail) begin.customerUserEmail = customerUserEmail;
|
|
3570
|
+
const stripeCustomerId = current.stripeCustomerId ?? base.stripeCustomerId;
|
|
3571
|
+
if (stripeCustomerId) begin.stripeCustomerId = stripeCustomerId;
|
|
3572
|
+
const batch = current.batch ?? base.batch;
|
|
3573
|
+
if (typeof batch === "boolean") begin.batch = batch;
|
|
3574
|
+
const pricingMode = current.pricingMode ?? base.pricingMode;
|
|
3575
|
+
if (pricingMode) begin.pricingMode = pricingMode;
|
|
3576
|
+
if (tags?.length) {
|
|
3577
|
+
begin.tags = tags;
|
|
3578
|
+
}
|
|
3579
|
+
return begin;
|
|
3580
|
+
}
|
|
3581
|
+
function responsesBeginRequest(begin, params) {
|
|
3582
|
+
if (!responsesRequestUsesWebSearch(params)) return begin;
|
|
3583
|
+
return {
|
|
3584
|
+
...begin,
|
|
3585
|
+
requested: {
|
|
3586
|
+
...begin.requested ?? {},
|
|
3587
|
+
search: true
|
|
58
3588
|
}
|
|
59
3589
|
};
|
|
60
3590
|
}
|
|
3591
|
+
function responsesRequestUsesWebSearch(params) {
|
|
3592
|
+
if (!params || typeof params !== "object") return false;
|
|
3593
|
+
const tools = params.tools;
|
|
3594
|
+
return Array.isArray(tools) && tools.some((tool) => {
|
|
3595
|
+
if (!tool || typeof tool !== "object") return false;
|
|
3596
|
+
const type = tool.type;
|
|
3597
|
+
return type === "web_search" || type === "web_search_preview";
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
function transformApiPromise(apiPromise, onResolve, onReject) {
|
|
3601
|
+
const resolvedPromise = Promise.resolve(apiPromise).then(onResolve, onReject);
|
|
3602
|
+
if (isObjectRecord(apiPromise)) {
|
|
3603
|
+
const proto = Object.getPrototypeOf(apiPromise);
|
|
3604
|
+
if (proto) {
|
|
3605
|
+
Object.setPrototypeOf(resolvedPromise, proto);
|
|
3606
|
+
}
|
|
3607
|
+
for (const key of Reflect.ownKeys(apiPromise)) {
|
|
3608
|
+
if (key === "then" || key === "catch" || key === "finally") {
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
try {
|
|
3612
|
+
const descriptor = Object.getOwnPropertyDescriptor(apiPromise, key);
|
|
3613
|
+
if (descriptor) {
|
|
3614
|
+
Reflect.defineProperty(resolvedPromise, key, descriptor);
|
|
3615
|
+
}
|
|
3616
|
+
} catch {
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return resolvedPromise;
|
|
3621
|
+
}
|
|
61
3622
|
function isObjectRecord(value) {
|
|
62
3623
|
return typeof value === "object" && value !== null;
|
|
63
3624
|
}
|
|
64
|
-
function
|
|
3625
|
+
function cloneRecord(value) {
|
|
3626
|
+
return isObjectRecord(value) ? { ...value } : {};
|
|
3627
|
+
}
|
|
3628
|
+
function isStringTuple(value) {
|
|
3629
|
+
return Array.isArray(value) && value.length >= 2 && typeof value[0] === "string" && typeof value[1] === "string";
|
|
3630
|
+
}
|
|
3631
|
+
function cloneRequestOptions(source) {
|
|
3632
|
+
const clone = { ...source };
|
|
3633
|
+
if ("headers" in clone) {
|
|
3634
|
+
clone.headers = normalizeHeaders(clone.headers);
|
|
3635
|
+
}
|
|
3636
|
+
return clone;
|
|
3637
|
+
}
|
|
3638
|
+
function attachCorrelationHeader(options, correlationId) {
|
|
3639
|
+
const normalized = normalizeHeaders(options?.headers);
|
|
3640
|
+
if (correlationId && !normalized[USAGETAP_CORRELATION_HEADER]) {
|
|
3641
|
+
normalized[USAGETAP_CORRELATION_HEADER] = correlationId;
|
|
3642
|
+
}
|
|
3643
|
+
if (!options) {
|
|
3644
|
+
return Object.keys(normalized).length ? { headers: normalized } : void 0;
|
|
3645
|
+
}
|
|
3646
|
+
const next = { ...options };
|
|
3647
|
+
if (Object.keys(normalized).length) {
|
|
3648
|
+
next.headers = normalized;
|
|
3649
|
+
}
|
|
3650
|
+
return next;
|
|
3651
|
+
}
|
|
3652
|
+
function normalizeHeaders(headers) {
|
|
3653
|
+
if (!headers) {
|
|
3654
|
+
return {};
|
|
3655
|
+
}
|
|
3656
|
+
if (headers instanceof Headers) {
|
|
3657
|
+
const result = {};
|
|
3658
|
+
headers.forEach((value, key) => {
|
|
3659
|
+
result[key.toLowerCase()] = value;
|
|
3660
|
+
});
|
|
3661
|
+
return result;
|
|
3662
|
+
}
|
|
3663
|
+
if (Array.isArray(headers)) {
|
|
3664
|
+
const result = {};
|
|
3665
|
+
for (const entry of headers) {
|
|
3666
|
+
if (!isStringTuple(entry)) {
|
|
3667
|
+
continue;
|
|
3668
|
+
}
|
|
3669
|
+
const [key, value] = entry;
|
|
3670
|
+
result[key.toLowerCase()] = value;
|
|
3671
|
+
}
|
|
3672
|
+
return result;
|
|
3673
|
+
}
|
|
3674
|
+
if (isObjectRecord(headers)) {
|
|
3675
|
+
const result = {};
|
|
3676
|
+
const record = headers;
|
|
3677
|
+
for (const key of Object.keys(record)) {
|
|
3678
|
+
const value = record[key];
|
|
3679
|
+
if (value !== void 0 && value !== null) {
|
|
3680
|
+
result[key.toLowerCase()] = String(value);
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
return result;
|
|
3684
|
+
}
|
|
3685
|
+
return {};
|
|
3686
|
+
}
|
|
3687
|
+
function mergeTags(a, b) {
|
|
3688
|
+
const values = [...a ?? [], ...b ?? []].map((value) => typeof value === "string" ? value.trim() : "").filter(Boolean);
|
|
3689
|
+
if (!values.length) {
|
|
3690
|
+
return void 0;
|
|
3691
|
+
}
|
|
3692
|
+
return dedupeStrings2(values);
|
|
3693
|
+
}
|
|
3694
|
+
function dedupeStrings2(values) {
|
|
3695
|
+
return Array.from(new Set(values));
|
|
3696
|
+
}
|
|
3697
|
+
function isStreamingRequest(params) {
|
|
3698
|
+
if (!params || typeof params !== "object") {
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
const stream = params.stream;
|
|
3702
|
+
if (typeof stream === "boolean") {
|
|
3703
|
+
return stream;
|
|
3704
|
+
}
|
|
3705
|
+
return stream != null;
|
|
3706
|
+
}
|
|
3707
|
+
function applyChatVendorHints(params, hints) {
|
|
3708
|
+
if (!hints) {
|
|
3709
|
+
return params;
|
|
3710
|
+
}
|
|
3711
|
+
const next = cloneRecord(params);
|
|
3712
|
+
if (hints.preferredModel && (next.model === void 0 || next.model === null)) {
|
|
3713
|
+
next.model = hints.preferredModel;
|
|
3714
|
+
}
|
|
3715
|
+
if (typeof hints.maxResponseTokens === "number" && next.max_tokens == null) {
|
|
3716
|
+
next.max_tokens = hints.maxResponseTokens;
|
|
3717
|
+
}
|
|
3718
|
+
if (typeof hints.maxInputTokens === "number" && next.max_input_tokens == null) {
|
|
3719
|
+
next.max_input_tokens = hints.maxInputTokens;
|
|
3720
|
+
}
|
|
3721
|
+
return next;
|
|
3722
|
+
}
|
|
3723
|
+
function applyResponsesVendorHints(params, hints) {
|
|
3724
|
+
if (!hints) {
|
|
3725
|
+
return params;
|
|
3726
|
+
}
|
|
3727
|
+
const next = cloneRecord(params);
|
|
3728
|
+
if (hints.preferredModel && (next.model === void 0 || next.model === null)) {
|
|
3729
|
+
next.model = hints.preferredModel;
|
|
3730
|
+
}
|
|
3731
|
+
if (typeof hints.maxResponseTokens === "number" && next.max_output_tokens == null) {
|
|
3732
|
+
next.max_output_tokens = hints.maxResponseTokens;
|
|
3733
|
+
}
|
|
3734
|
+
return next;
|
|
3735
|
+
}
|
|
3736
|
+
async function extractUsageFromStream(stream, hints, provider = "openai") {
|
|
3737
|
+
const finalPayload = await resolveStreamFinalPayload(stream);
|
|
3738
|
+
if (!finalPayload) {
|
|
3739
|
+
return void 0;
|
|
3740
|
+
}
|
|
3741
|
+
return inferUsageFromResponse(finalPayload, hints, provider);
|
|
3742
|
+
}
|
|
3743
|
+
async function resolveStreamFinalPayload(stream) {
|
|
3744
|
+
if (!stream || typeof stream !== "object") {
|
|
3745
|
+
return void 0;
|
|
3746
|
+
}
|
|
3747
|
+
const candidate = stream;
|
|
3748
|
+
if (typeof candidate.finalChatCompletion === "function") {
|
|
3749
|
+
return candidate.finalChatCompletion();
|
|
3750
|
+
}
|
|
3751
|
+
if (typeof candidate.finalResponse === "function") {
|
|
3752
|
+
return candidate.finalResponse();
|
|
3753
|
+
}
|
|
3754
|
+
if (typeof candidate.finalCompletion === "function") {
|
|
3755
|
+
return candidate.finalCompletion();
|
|
3756
|
+
}
|
|
3757
|
+
if (typeof candidate.finalContent === "function") {
|
|
3758
|
+
return candidate.finalContent();
|
|
3759
|
+
}
|
|
3760
|
+
return void 0;
|
|
3761
|
+
}
|
|
3762
|
+
function ensureAsyncIterable(value, label) {
|
|
3763
|
+
if (!value || typeof value !== "object" || typeof value[Symbol.asyncIterator] !== "function") {
|
|
3764
|
+
throw new UsageTapError(
|
|
3765
|
+
"USAGETAP_BAD_REQUEST",
|
|
3766
|
+
`${label} expected an async iterable stream but received ${typeof value}`
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
function chunkToText(chunk) {
|
|
3771
|
+
if (chunk === void 0 || chunk === null) {
|
|
3772
|
+
return "";
|
|
3773
|
+
}
|
|
3774
|
+
if (typeof chunk === "string") {
|
|
3775
|
+
return chunk;
|
|
3776
|
+
}
|
|
3777
|
+
if (typeof chunk === "object") {
|
|
3778
|
+
const candidate = chunk;
|
|
3779
|
+
const delta = candidate.choices?.[0]?.delta;
|
|
3780
|
+
const content = delta?.content ?? candidate.content;
|
|
3781
|
+
if (typeof content === "string") {
|
|
3782
|
+
return content;
|
|
3783
|
+
}
|
|
3784
|
+
if (Array.isArray(content)) {
|
|
3785
|
+
return content.map((entry) => {
|
|
3786
|
+
if (!entry) return "";
|
|
3787
|
+
if (typeof entry === "string") return entry;
|
|
3788
|
+
if (typeof entry.text === "string") return entry.text;
|
|
3789
|
+
return "";
|
|
3790
|
+
}).join("");
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
return String(chunk);
|
|
3794
|
+
}
|
|
3795
|
+
function formatSsePayload(text, options) {
|
|
3796
|
+
if (!text) {
|
|
3797
|
+
return "";
|
|
3798
|
+
}
|
|
3799
|
+
const lines = text.split(/\r?\n/);
|
|
3800
|
+
const eventLine = options?.event ? `event: ${options.event}
|
|
3801
|
+
` : "";
|
|
3802
|
+
const retryLine = options?.retry ? `retry: ${options.retry}
|
|
3803
|
+
` : "";
|
|
3804
|
+
const dataLines = lines.map((line) => `data: ${line}`).join("\n");
|
|
3805
|
+
return `${eventLine}${retryLine}${dataLines}
|
|
3806
|
+
|
|
3807
|
+
`;
|
|
3808
|
+
}
|
|
3809
|
+
function setHeaderIfPossible(res, key, value) {
|
|
3810
|
+
if (typeof res.setHeader === "function" && res.headersSent !== true) {
|
|
3811
|
+
res.setHeader(key, value);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
function tryInferUsage(response, hints, extractor, ctx, provider = "openai") {
|
|
65
3815
|
const explicit = extractor?.(response);
|
|
66
|
-
const inferred = explicit ?? inferUsageFromResponse(response, hints);
|
|
3816
|
+
const inferred = explicit ?? inferUsageFromResponse(response, hints, provider);
|
|
67
3817
|
if (inferred) {
|
|
68
3818
|
ctx.setUsage(inferred);
|
|
69
3819
|
}
|
|
70
3820
|
}
|
|
71
|
-
function inferUsageFromResponse(response, hints) {
|
|
3821
|
+
function inferUsageFromResponse(response, hints, provider = "openai") {
|
|
72
3822
|
if (!response || typeof response !== "object") {
|
|
73
3823
|
return void 0;
|
|
74
3824
|
}
|
|
@@ -76,32 +3826,110 @@ function inferUsageFromResponse(response, hints) {
|
|
|
76
3826
|
if (!candidate.usage) {
|
|
77
3827
|
return void 0;
|
|
78
3828
|
}
|
|
79
|
-
const cachedInputTokens = candidate.usage.prompt_tokens_details?.cached_tokens ?? candidate.usage.cached_tokens;
|
|
3829
|
+
const cachedInputTokens = candidate.usage.prompt_tokens_details?.cached_tokens ?? candidate.usage.input_tokens_details?.cached_tokens ?? candidate.usage.cached_tokens;
|
|
3830
|
+
const cacheWriteInputTokens = candidate.usage.prompt_tokens_details?.cache_write_tokens ?? candidate.usage.prompt_tokens_details?.cache_creation_tokens ?? candidate.usage.input_tokens_details?.cache_write_tokens ?? candidate.usage.input_tokens_details?.cache_creation_tokens ?? candidate.usage.cache_creation_input_tokens ?? candidate.usage.cache_write_input_tokens ?? candidate.usage.cache_write_tokens;
|
|
3831
|
+
const cacheWrite5mInputTokens = candidate.usage.cache_write_5m_input_tokens ?? candidate.usage.cache_creation?.ephemeral_5m_input_tokens;
|
|
3832
|
+
const cacheWrite1hInputTokens = candidate.usage.cache_write_1h_input_tokens ?? candidate.usage.cache_creation?.ephemeral_1h_input_tokens;
|
|
3833
|
+
const outputSearches = Array.isArray(candidate.output) ? candidate.output.filter((item) => item?.type === "web_search_call").length : 0;
|
|
3834
|
+
const searches = candidate.usage.searches ?? candidate.usage.web_search_queries ?? candidate.usage.server_tool_use?.web_search_requests ?? outputSearches;
|
|
3835
|
+
const responseEffort = normalizeExecutionReasoningEffort(
|
|
3836
|
+
candidate.reasoning?.effort
|
|
3837
|
+
);
|
|
80
3838
|
return {
|
|
3839
|
+
providerUsed: provider,
|
|
81
3840
|
modelUsed: candidate.model ?? hints?.preferredModel,
|
|
82
|
-
inputTokens: candidate.usage.prompt_tokens,
|
|
83
|
-
responseTokens: candidate.usage.completion_tokens,
|
|
84
|
-
cachedInputTokens
|
|
3841
|
+
inputTokens: candidate.usage.prompt_tokens ?? candidate.usage.input_tokens,
|
|
3842
|
+
responseTokens: candidate.usage.completion_tokens ?? candidate.usage.output_tokens,
|
|
3843
|
+
cachedInputTokens,
|
|
3844
|
+
cacheWriteInputTokens,
|
|
3845
|
+
cacheWrite5mInputTokens,
|
|
3846
|
+
cacheWrite1hInputTokens,
|
|
3847
|
+
audioInputTokens: candidate.usage.prompt_tokens_details?.audio_tokens ?? candidate.usage.input_tokens_details?.audio_tokens,
|
|
3848
|
+
cachedAudioInputTokens: candidate.usage.prompt_tokens_details?.cached_audio_tokens ?? candidate.usage.input_tokens_details?.cached_audio_tokens ?? candidate.usage.prompt_tokens_details?.cached_tokens_details?.audio_tokens ?? candidate.usage.input_tokens_details?.cached_tokens_details?.audio_tokens,
|
|
3849
|
+
imageInputTokens: candidate.usage.prompt_tokens_details?.image_tokens ?? candidate.usage.input_tokens_details?.image_tokens,
|
|
3850
|
+
imageOutputTokens: candidate.usage.completion_tokens_details?.image_tokens ?? candidate.usage.output_tokens_details?.image_tokens,
|
|
3851
|
+
audioOutputTokens: candidate.usage.completion_tokens_details?.audio_tokens ?? candidate.usage.output_tokens_details?.audio_tokens,
|
|
3852
|
+
reasoningTokens: candidate.usage.completion_tokens_details?.reasoning_tokens ?? candidate.usage.output_tokens_details?.reasoning_tokens,
|
|
3853
|
+
...responseEffort ? {
|
|
3854
|
+
reasoningEffort: responseEffort,
|
|
3855
|
+
reasoningEffortSource: "provider_response"
|
|
3856
|
+
} : {},
|
|
3857
|
+
...typeof candidate.reasoning?.type === "string" ? { reasoningMode: candidate.reasoning.type } : typeof candidate.reasoning?.mode === "string" ? { reasoningMode: candidate.reasoning.mode } : {},
|
|
3858
|
+
...typeof searches === "number" && searches > 0 ? { searches } : {}
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
function normalizeExecutionReasoningEffort(value) {
|
|
3862
|
+
return value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max" ? value : void 0;
|
|
3863
|
+
}
|
|
3864
|
+
function openAIRequestExecutionMetadata(params, provider) {
|
|
3865
|
+
const record = params && typeof params === "object" ? params : {};
|
|
3866
|
+
const reasoning = record.reasoning && typeof record.reasoning === "object" ? record.reasoning : void 0;
|
|
3867
|
+
const effort = normalizeExecutionReasoningEffort(
|
|
3868
|
+
record.reasoning_effort ?? reasoning?.effort ?? record.thinking_level
|
|
3869
|
+
);
|
|
3870
|
+
const mode = typeof reasoning?.type === "string" ? reasoning.type : typeof reasoning?.mode === "string" ? reasoning.mode : void 0;
|
|
3871
|
+
const rawBudget = reasoning?.budget_tokens ?? record.thinking_budget ?? record.thinking_budget_tokens;
|
|
3872
|
+
const budget = typeof rawBudget === "number" && Number.isInteger(rawBudget) && rawBudget >= 0 ? rawBudget : void 0;
|
|
3873
|
+
return {
|
|
3874
|
+
providerUsed: provider,
|
|
3875
|
+
...typeof record.model === "string" ? { modelUsed: record.model } : {},
|
|
3876
|
+
...effort ? { reasoningEffort: effort, reasoningEffortSource: "provider_request" } : {},
|
|
3877
|
+
...mode ? { reasoningMode: mode } : {},
|
|
3878
|
+
...budget !== void 0 ? { reasoningBudgetTokens: budget } : {}
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
function tryInferUsageFromStreamChunk(chunk, hints, ctx, provider) {
|
|
3882
|
+
const payload = isObjectRecord(chunk) && isObjectRecord(chunk.response) ? chunk.response : chunk;
|
|
3883
|
+
const inferred = inferUsageFromResponse(payload, hints, provider);
|
|
3884
|
+
if (inferred) {
|
|
3885
|
+
ctx.setUsage(inferred);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
function ensureOpenAIStreamUsage(params) {
|
|
3889
|
+
if (!isObjectRecord(params)) {
|
|
3890
|
+
return params;
|
|
3891
|
+
}
|
|
3892
|
+
const streamOptions = isObjectRecord(params.stream_options) ? params.stream_options : {};
|
|
3893
|
+
return {
|
|
3894
|
+
...params,
|
|
3895
|
+
stream_options: {
|
|
3896
|
+
...streamOptions,
|
|
3897
|
+
include_usage: true
|
|
3898
|
+
}
|
|
85
3899
|
};
|
|
86
3900
|
}
|
|
87
|
-
function
|
|
3901
|
+
function deferUsageFinalization(ctx) {
|
|
3902
|
+
return ctx.deferFinalization?.() ?? (() => Promise.resolve());
|
|
3903
|
+
}
|
|
3904
|
+
function wrapStreamForUsageTap(source, finalize, ctx, onChunk) {
|
|
88
3905
|
const getIterator = source[Symbol.asyncIterator];
|
|
89
3906
|
if (typeof getIterator !== "function") {
|
|
90
3907
|
throw new TypeError("Stream is not async iterable");
|
|
91
3908
|
}
|
|
92
3909
|
const iterator = getIterator.call(source);
|
|
93
3910
|
let completed = false;
|
|
94
|
-
const invokeFinalize = async () => {
|
|
3911
|
+
const invokeFinalize = async (termination, error) => {
|
|
95
3912
|
if (completed) return;
|
|
96
3913
|
completed = true;
|
|
3914
|
+
if (termination === "cancel" || termination === "manual") {
|
|
3915
|
+
ctx.setError({
|
|
3916
|
+
code: "STREAM_ABORTED",
|
|
3917
|
+
message: termination === "cancel" ? "Provider stream consumption was cancelled before completion" : "Provider stream was finalized before completion"
|
|
3918
|
+
});
|
|
3919
|
+
} else if (termination === "error") {
|
|
3920
|
+
ctx.setError({
|
|
3921
|
+
code: "VENDOR_ERROR",
|
|
3922
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
97
3925
|
try {
|
|
98
|
-
await finalize();
|
|
99
|
-
} catch (
|
|
3926
|
+
await finalize(termination);
|
|
3927
|
+
} catch (error2) {
|
|
100
3928
|
ctx.setError({
|
|
101
3929
|
code: "USAGE_FINALIZE_ERROR",
|
|
102
|
-
message:
|
|
3930
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
103
3931
|
});
|
|
104
|
-
throw
|
|
3932
|
+
throw error2;
|
|
105
3933
|
}
|
|
106
3934
|
};
|
|
107
3935
|
const prototype = Object.getPrototypeOf(source) ?? Object.prototype;
|
|
@@ -125,12 +3953,15 @@ function wrapStreamForUsageTap(source, finalize, ctx) {
|
|
|
125
3953
|
value: async (...args) => {
|
|
126
3954
|
try {
|
|
127
3955
|
const result = await iterator.next(...args);
|
|
3956
|
+
if (!result.done) {
|
|
3957
|
+
onChunk?.(result.value);
|
|
3958
|
+
}
|
|
128
3959
|
if (result.done) {
|
|
129
|
-
await invokeFinalize();
|
|
3960
|
+
await invokeFinalize("complete");
|
|
130
3961
|
}
|
|
131
3962
|
return result;
|
|
132
3963
|
} catch (error) {
|
|
133
|
-
await invokeFinalize().catch(() => void 0);
|
|
3964
|
+
await invokeFinalize("error", error).catch(() => void 0);
|
|
134
3965
|
throw error;
|
|
135
3966
|
}
|
|
136
3967
|
},
|
|
@@ -139,39 +3970,49 @@ function wrapStreamForUsageTap(source, finalize, ctx) {
|
|
|
139
3970
|
});
|
|
140
3971
|
Object.defineProperty(wrapped, "return", {
|
|
141
3972
|
value: async (value) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
3973
|
+
try {
|
|
3974
|
+
if (typeof iterator.return === "function") {
|
|
3975
|
+
const rawResult = await iterator.return(value);
|
|
3976
|
+
if (!isIteratorResult(rawResult)) {
|
|
3977
|
+
throw new TypeError("Iterator.return() returned an invalid result");
|
|
3978
|
+
}
|
|
3979
|
+
await invokeFinalize("cancel");
|
|
3980
|
+
return rawResult;
|
|
146
3981
|
}
|
|
147
|
-
await invokeFinalize();
|
|
148
|
-
return
|
|
3982
|
+
await invokeFinalize("cancel");
|
|
3983
|
+
return { done: true, value };
|
|
3984
|
+
} catch (error) {
|
|
3985
|
+
await invokeFinalize("error", error).catch(() => void 0);
|
|
3986
|
+
throw error;
|
|
149
3987
|
}
|
|
150
|
-
await invokeFinalize();
|
|
151
|
-
return { done: true, value };
|
|
152
3988
|
},
|
|
153
3989
|
configurable: true,
|
|
154
3990
|
writable: true
|
|
155
3991
|
});
|
|
156
3992
|
Object.defineProperty(wrapped, "throw", {
|
|
157
3993
|
value: async (error) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
3994
|
+
try {
|
|
3995
|
+
if (typeof iterator.throw === "function") {
|
|
3996
|
+
const rawResult = await iterator.throw(error);
|
|
3997
|
+
if (!isIteratorResult(rawResult)) {
|
|
3998
|
+
throw new TypeError("Iterator.throw() returned an invalid result");
|
|
3999
|
+
}
|
|
4000
|
+
await invokeFinalize("error", error);
|
|
4001
|
+
return rawResult;
|
|
162
4002
|
}
|
|
163
|
-
await invokeFinalize();
|
|
164
|
-
|
|
4003
|
+
await invokeFinalize("error", error);
|
|
4004
|
+
throw error;
|
|
4005
|
+
} catch (thrownError) {
|
|
4006
|
+
await invokeFinalize("error", thrownError).catch(() => void 0);
|
|
4007
|
+
throw thrownError;
|
|
165
4008
|
}
|
|
166
|
-
await invokeFinalize();
|
|
167
|
-
throw error;
|
|
168
4009
|
},
|
|
169
4010
|
configurable: true,
|
|
170
4011
|
writable: true
|
|
171
4012
|
});
|
|
172
4013
|
Object.defineProperty(wrapped, "__usageTapFinalize", {
|
|
173
4014
|
value: async () => {
|
|
174
|
-
await invokeFinalize();
|
|
4015
|
+
await invokeFinalize("manual");
|
|
175
4016
|
},
|
|
176
4017
|
configurable: true
|
|
177
4018
|
});
|
|
@@ -183,9 +4024,24 @@ function isIteratorResult(value) {
|
|
|
183
4024
|
|
|
184
4025
|
// src/adapters/openrouter.ts
|
|
185
4026
|
function createOpenRouterAdapter(init) {
|
|
186
|
-
return createOpenAIAdapter(init);
|
|
4027
|
+
return createOpenAIAdapter({ ...init, provider: "openrouter" });
|
|
4028
|
+
}
|
|
4029
|
+
function withMetering2(client, customer) {
|
|
4030
|
+
return withMetering(
|
|
4031
|
+
client,
|
|
4032
|
+
typeof customer === "string" ? { customerId: customer, provider: "openrouter" } : { ...customer, provider: "openrouter" }
|
|
4033
|
+
);
|
|
4034
|
+
}
|
|
4035
|
+
function wrapOpenAI2(client, usageTap, options = {}) {
|
|
4036
|
+
return wrapOpenAI(client, usageTap, {
|
|
4037
|
+
...options,
|
|
4038
|
+
provider: "openrouter"
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
function withSampling2(client, options = {}) {
|
|
4042
|
+
return withSampling(client, { ...options, provider: "openrouter" });
|
|
187
4043
|
}
|
|
188
4044
|
|
|
189
|
-
export { createOpenRouterAdapter };
|
|
4045
|
+
export { createOpenRouterAdapter, withMetering2 as withMetering, withSampling2 as withSampling, wrapOpenAI2 as wrapOpenAI };
|
|
190
4046
|
//# sourceMappingURL=openrouter.mjs.map
|
|
191
4047
|
//# sourceMappingURL=openrouter.mjs.map
|