@usagetap/sdk 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -115,9 +115,468 @@ async function runWithRetry(operation, options, shouldRetry, onSchedule, signal)
115
115
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
116
116
  }
117
117
 
118
+ // src/prompt-compression.ts
119
+ var DEFAULT_TTC_ENDPOINT = "https://api.thetokencompany.com/v1/compress";
120
+ async function compressPrompt(options) {
121
+ try {
122
+ if (options.provider === "thetokencompany" || options.tokenCompanyApiKey) {
123
+ return await compressWithTheTokenCompany(options);
124
+ }
125
+ if (options.provider === "toon") {
126
+ return compressPromptToon(options.input);
127
+ }
128
+ return compressPromptHeuristic(options.input);
129
+ } catch (error) {
130
+ if (options.failOpen === false) {
131
+ throw error;
132
+ }
133
+ return createPromptCompressionFallback(
134
+ options.input,
135
+ options.provider ?? (options.tokenCompanyApiKey ? "thetokencompany" : "heuristic"),
136
+ error
137
+ );
138
+ }
139
+ }
140
+ function compressPromptHeuristic(input) {
141
+ const original = stableStringifyInput(input);
142
+ const techniques = /* @__PURE__ */ new Set();
143
+ const compressedInput = compressValue(input, techniques, { allowToonString: false });
144
+ const compressed = stableStringifyInput(compressedInput);
145
+ const chosenInput = compressed.length <= original.length ? compressedInput : input;
146
+ const chosen = compressed.length <= original.length ? compressed : original;
147
+ if (!techniques.size) {
148
+ techniques.add("no-op");
149
+ }
150
+ return buildResult(
151
+ input,
152
+ chosenInput,
153
+ "heuristic",
154
+ original,
155
+ chosen,
156
+ Array.from(techniques)
157
+ );
158
+ }
159
+ function compressPromptToon(input) {
160
+ const original = stableStringifyInput(input);
161
+ const compressedInput = typeof input === "string" ? compressText(input, /* @__PURE__ */ new Set(), { allowToonString: true }) : encodeToon(input);
162
+ const compressed = stableStringifyInput(compressedInput);
163
+ return buildResult(input, compressedInput, "toon", original, compressed, [
164
+ "toon",
165
+ "json-minify"
166
+ ]);
167
+ }
168
+ async function compressWithTheTokenCompany(options) {
169
+ if (!options.tokenCompanyApiKey) {
170
+ throw new Error(
171
+ "tokenCompanyApiKey is required when provider is thetokencompany"
172
+ );
173
+ }
174
+ const fetchCandidate = options.fetchImpl ?? globalThis.fetch;
175
+ if (typeof fetchCandidate !== "function") {
176
+ throw new Error(
177
+ "A fetch implementation is required for The Token Company compression"
178
+ );
179
+ }
180
+ const original = stableStringifyInput(options.input);
181
+ const heuristic = compressPromptHeuristic(options.input);
182
+ const response = await fetchCandidate(
183
+ options.tokenCompanyEndpoint ?? DEFAULT_TTC_ENDPOINT,
184
+ {
185
+ method: "POST",
186
+ headers: {
187
+ authorization: `Bearer ${options.tokenCompanyApiKey}`,
188
+ "content-type": "application/json"
189
+ },
190
+ body: JSON.stringify({ input: heuristic.compressedInput }),
191
+ signal: options.signal
192
+ }
193
+ );
194
+ if (!response.ok) {
195
+ throw new Error(
196
+ `The Token Company compression failed with HTTP ${response.status}`
197
+ );
198
+ }
199
+ const payload = await response.json();
200
+ const tokenCompanyResult = normalizeTheTokenCompanyCompressResponse(payload);
201
+ const compressedInput = payload.compressedInput ?? payload.compressed ?? tokenCompanyResult?.output ?? payload.output ?? payload.text;
202
+ if (compressedInput === void 0) {
203
+ throw new Error("The Token Company response did not include compressed content");
204
+ }
205
+ const compressed = stableStringifyInput(compressedInput);
206
+ const tokenCounts = tokenCompanyResult ? {
207
+ originalTokens: tokenCompanyResult.input_tokens,
208
+ compressedTokens: tokenCompanyResult.output_tokens,
209
+ savedTokens: tokenCompanyResult.tokens_saved
210
+ } : void 0;
211
+ return buildResult(
212
+ options.input,
213
+ compressedInput,
214
+ "thetokencompany",
215
+ original,
216
+ compressed,
217
+ [...heuristic.techniques, "thetokencompany"],
218
+ tokenCounts
219
+ );
220
+ }
221
+ function normalizeTheTokenCompanyCompressResponse(data) {
222
+ if (typeof data.output !== "string" || typeof data.output_tokens !== "number") {
223
+ return void 0;
224
+ }
225
+ const inputTokens = typeof data.input_tokens === "number" ? data.input_tokens : data.original_input_tokens;
226
+ if (typeof inputTokens !== "number") {
227
+ return void 0;
228
+ }
229
+ const tokensSaved = typeof data.tokens_saved === "number" ? data.tokens_saved : inputTokens - data.output_tokens;
230
+ const compressionRatio = typeof data.compression_ratio === "number" ? data.compression_ratio : data.output_tokens === 0 ? 0 : inputTokens / data.output_tokens;
231
+ return {
232
+ output: data.output,
233
+ output_tokens: data.output_tokens,
234
+ input_tokens: inputTokens,
235
+ tokens_saved: tokensSaved,
236
+ compression_ratio: compressionRatio
237
+ };
238
+ }
239
+ function createPromptCompressionFallback(input, provider = "heuristic", error) {
240
+ const original = stableStringifyInput(input);
241
+ const techniques = ["fallback-original"];
242
+ if (error) {
243
+ techniques.push("compression-error");
244
+ }
245
+ return buildResult(input, input, provider, original, original, techniques);
246
+ }
247
+ function buildResult(input, compressedInput, provider, original, compressed, techniques, tokenCounts) {
248
+ const originalCharacters = original.length;
249
+ const compressedCharacters = compressed.length;
250
+ const savedCharacters = Math.max(
251
+ 0,
252
+ originalCharacters - compressedCharacters
253
+ );
254
+ const originalTokens = tokenCounts?.originalTokens ?? estimatePromptTokens(original);
255
+ const compressedTokens = tokenCounts?.compressedTokens ?? estimatePromptTokens(compressed);
256
+ const savedTokens = Math.max(
257
+ 0,
258
+ tokenCounts?.savedTokens ?? originalTokens - compressedTokens
259
+ );
260
+ return {
261
+ input,
262
+ compressedInput,
263
+ provider,
264
+ originalCharacters,
265
+ compressedCharacters,
266
+ savedCharacters,
267
+ originalTokens,
268
+ compressedTokens,
269
+ savedTokens,
270
+ tokenSavingsRatio: originalTokens > 0 ? savedTokens / originalTokens : 0,
271
+ savingsRatio: originalCharacters > 0 ? savedCharacters / originalCharacters : 0,
272
+ techniques
273
+ };
274
+ }
275
+ function estimatePromptTokens(input) {
276
+ const text = typeof input === "string" ? input : stableStringifyInput(input);
277
+ return text.match(/[\p{L}\p{N}]+|[^\s]/gu)?.length ?? 0;
278
+ }
279
+ function compressValue(value, techniques, options) {
280
+ if (typeof value === "string") return compressText(value, techniques, options);
281
+ if (Array.isArray(value)) {
282
+ techniques.add("json-minify");
283
+ return value.map((item) => compressValue(item, techniques, options));
284
+ }
285
+ if (value && typeof value === "object") {
286
+ techniques.add("json-minify");
287
+ return Object.keys(value).reduce((acc, key) => {
288
+ const child = value[key];
289
+ if (child !== void 0) {
290
+ acc[key] = compressValue(child, techniques, options);
291
+ }
292
+ return acc;
293
+ }, {});
294
+ }
295
+ return value;
296
+ }
297
+ function compressText(value, techniques, options) {
298
+ const fencePattern = /```([\w-]+)?\n([\s\S]*?)```/g;
299
+ const parts = [];
300
+ let cursor = 0;
301
+ let match;
302
+ while ((match = fencePattern.exec(value)) !== null) {
303
+ const before = value.slice(cursor, match.index);
304
+ const compressedBefore = compressPlainTextAndEmbeddedJson(
305
+ before,
306
+ techniques,
307
+ options
308
+ );
309
+ if (compressedBefore) parts.push(compressedBefore);
310
+ const lang = match[1]?.toLowerCase();
311
+ const code = cleanCodeBlock(match[2] ?? "");
312
+ const compressedCode = lang === "json" ? compressJsonText(code, techniques, options) : void 0;
313
+ if (compressedCode?.format === "toon") {
314
+ parts.push(`\`\`\`toon
315
+ ${compressedCode.text}
316
+ \`\`\``);
317
+ } else if (compressedCode?.format === "json") {
318
+ parts.push(`\`\`\`json
319
+ ${compressedCode.text}
320
+ \`\`\``);
321
+ } else {
322
+ if (code !== match[2]) {
323
+ techniques.add("code-whitespace");
324
+ }
325
+ parts.push(lang ? `\`\`\`${lang}
326
+ ${code}
327
+ \`\`\`` : `\`\`\`
328
+ ${code}
329
+ \`\`\``);
330
+ }
331
+ cursor = match.index + match[0].length;
332
+ }
333
+ const after = compressPlainTextAndEmbeddedJson(value.slice(cursor), techniques, options);
334
+ if (after) parts.push(after);
335
+ return parts.join("\n").trim();
336
+ }
337
+ function compressPlainText(value, techniques) {
338
+ const compressed = value.split("\n").map((line) => line.trim()).filter((line) => line).join("\n").replace(/[ \t]{2,}/g, " ").trim();
339
+ if (compressed !== value.trim()) {
340
+ techniques.add("text-whitespace");
341
+ }
342
+ return compressed;
343
+ }
344
+ function compressPlainTextAndEmbeddedJson(value, techniques, options) {
345
+ const normalized = compressPlainText(value, techniques);
346
+ return compressEmbeddedJson(normalized, techniques, options);
347
+ }
348
+ function cleanCodeBlock(code) {
349
+ const lines = code.replace(/\r\n/g, "\n").split("\n");
350
+ while (lines.length && lines[0].trim() === "") lines.shift();
351
+ while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
352
+ const commonIndent = lines.filter((line) => line.trim()).reduce((min, line) => {
353
+ const indent = /^[ \t]*/.exec(line)?.[0].length ?? 0;
354
+ return min === void 0 ? indent : Math.min(min, indent);
355
+ }, void 0);
356
+ return lines.map((line) => commonIndent ? line.slice(commonIndent) : line).join("\n").replace(/[ \t]+$/gm, "");
357
+ }
358
+ function stableStringifyInput(input) {
359
+ if (typeof input === "string") return input;
360
+ return JSON.stringify(input) ?? String(input);
361
+ }
362
+ function compressJsonText(text, techniques, options) {
363
+ const parsed = safeParseJson(text);
364
+ if (parsed === void 0) {
365
+ return void 0;
366
+ }
367
+ const compactJson = JSON.stringify(parsed);
368
+ const candidates = [
369
+ { format: "json", text: compactJson }
370
+ ];
371
+ if (options.allowToonString || shouldUseToonForJson(parsed)) {
372
+ candidates.push({ format: "toon", text: encodeToon(parsed) });
373
+ }
374
+ const originalLength = text.trim().length;
375
+ const best = candidates.reduce(
376
+ (winner, candidate) => candidate.text.length < winner.text.length ? candidate : winner
377
+ );
378
+ if (best.text.length >= originalLength) {
379
+ return void 0;
380
+ }
381
+ techniques.add(best.format === "toon" ? "embedded-json-toon" : "embedded-json-minify");
382
+ return best;
383
+ }
384
+ function compressEmbeddedJson(text, techniques, options) {
385
+ let result = "";
386
+ let cursor = 0;
387
+ while (cursor < text.length) {
388
+ const start = findNextJsonStart(text, cursor);
389
+ if (start < 0) {
390
+ result += text.slice(cursor);
391
+ break;
392
+ }
393
+ result += text.slice(cursor, start);
394
+ const span = findBalancedJsonSpan(text, start);
395
+ if (!span) {
396
+ result += text[start];
397
+ cursor = start + 1;
398
+ continue;
399
+ }
400
+ const candidate = compressJsonText(span.text, techniques, options);
401
+ if (candidate) {
402
+ result += candidate.text;
403
+ } else {
404
+ result += span.text;
405
+ }
406
+ cursor = span.end;
407
+ }
408
+ return result;
409
+ }
410
+ function findNextJsonStart(text, from) {
411
+ const objectStart = text.indexOf("{", from);
412
+ const arrayStart = text.indexOf("[", from);
413
+ if (objectStart < 0) return arrayStart;
414
+ if (arrayStart < 0) return objectStart;
415
+ return Math.min(objectStart, arrayStart);
416
+ }
417
+ function findBalancedJsonSpan(text, start) {
418
+ const opener = text[start];
419
+ const closer = opener === "{" ? "}" : opener === "[" ? "]" : void 0;
420
+ if (!closer) return void 0;
421
+ const stack = [closer];
422
+ let inString = false;
423
+ let escaped = false;
424
+ for (let index = start + 1; index < text.length; index += 1) {
425
+ const char = text[index];
426
+ if (inString) {
427
+ if (escaped) {
428
+ escaped = false;
429
+ } else if (char === "\\") {
430
+ escaped = true;
431
+ } else if (char === '"') {
432
+ inString = false;
433
+ }
434
+ continue;
435
+ }
436
+ if (char === '"') {
437
+ inString = true;
438
+ continue;
439
+ }
440
+ if (char === "{" || char === "[") {
441
+ stack.push(char === "{" ? "}" : "]");
442
+ continue;
443
+ }
444
+ if (char === stack[stack.length - 1]) {
445
+ stack.pop();
446
+ if (!stack.length) {
447
+ const end = index + 1;
448
+ return { text: text.slice(start, end), end };
449
+ }
450
+ }
451
+ }
452
+ return void 0;
453
+ }
454
+ function safeParseJson(text) {
455
+ try {
456
+ return JSON.parse(text);
457
+ } catch {
458
+ return void 0;
459
+ }
460
+ }
461
+ function shouldUseToonForJson(value) {
462
+ if (Array.isArray(value)) {
463
+ return isUniformObjectArray(value) || value.some(shouldUseToonForJson);
464
+ }
465
+ if (isPlainObject(value)) {
466
+ return Object.values(value).some(shouldUseToonForJson);
467
+ }
468
+ return false;
469
+ }
470
+ function encodeToon(value, indent = 0) {
471
+ if (isPrimitive(value)) {
472
+ return scalarToToon(value);
473
+ }
474
+ if (Array.isArray(value)) {
475
+ return encodeArrayToon(value, indent);
476
+ }
477
+ if (isPlainObject(value)) {
478
+ const lines = [];
479
+ for (const [key, child] of Object.entries(value)) {
480
+ lines.push(...encodePropertyToon(key, child, indent));
481
+ }
482
+ return lines.join("\n");
483
+ }
484
+ return scalarToToon(String(value));
485
+ }
486
+ function encodePropertyToon(key, value, indent) {
487
+ const prefix = " ".repeat(indent);
488
+ const toonKey = keyToToon(key);
489
+ if (isPrimitive(value)) {
490
+ return [`${prefix}${toonKey}: ${scalarToToon(value)}`];
491
+ }
492
+ if (Array.isArray(value)) {
493
+ if (value.every(isPrimitive)) {
494
+ return [`${prefix}${toonKey}[${value.length}]: ${value.map(scalarToToon).join(",")}`];
495
+ }
496
+ if (isUniformObjectArray(value)) {
497
+ const fields = Object.keys(value[0]);
498
+ const header = `${prefix}${toonKey}[${value.length}]{${fields.map(keyToToon).join(",")}}:`;
499
+ const rows = value.map(
500
+ (item) => `${" ".repeat(indent + 2)}${fields.map(
501
+ (field) => scalarToToon(item[field])
502
+ ).join(",")}`
503
+ );
504
+ return [header, ...rows];
505
+ }
506
+ return [
507
+ `${prefix}${toonKey}[${value.length}]:`,
508
+ ...value.flatMap((item, index) => {
509
+ if (isPrimitive(item)) {
510
+ return [`${" ".repeat(indent + 2)}- ${scalarToToon(item)}`];
511
+ }
512
+ return [
513
+ `${" ".repeat(indent + 2)}- item${index}:`,
514
+ ...encodeToon(item, indent + 4).split("\n")
515
+ ];
516
+ })
517
+ ];
518
+ }
519
+ return [`${prefix}${toonKey}:`, ...encodeToon(value, indent + 2).split("\n")];
520
+ }
521
+ function encodeArrayToon(value, indent) {
522
+ if (value.every(isPrimitive)) {
523
+ return `[${value.length}]: ${value.map(scalarToToon).join(",")}`;
524
+ }
525
+ if (isUniformObjectArray(value)) {
526
+ const fields = Object.keys(value[0]);
527
+ return [
528
+ `[${value.length}]{${fields.map(keyToToon).join(",")}}:`,
529
+ ...value.map(
530
+ (item) => `${" ".repeat(indent + 2)}${fields.map(
531
+ (field) => scalarToToon(item[field])
532
+ ).join(",")}`
533
+ )
534
+ ].join("\n");
535
+ }
536
+ return value.flatMap((item, index) => [
537
+ `${" ".repeat(indent)}- item${index}:`,
538
+ ...encodeToon(item, indent + 2).split("\n")
539
+ ]).join("\n");
540
+ }
541
+ function isUniformObjectArray(value) {
542
+ if (!value.length || !value.every(isPlainObject)) {
543
+ return false;
544
+ }
545
+ const fields = Object.keys(value[0]);
546
+ if (!fields.length) {
547
+ return false;
548
+ }
549
+ return value.every((item) => {
550
+ const record = item;
551
+ const itemFields = Object.keys(record);
552
+ return itemFields.length === fields.length && fields.every((field) => itemFields.includes(field) && isPrimitive(record[field]));
553
+ });
554
+ }
555
+ function isPlainObject(value) {
556
+ return typeof value === "object" && value !== null && !Array.isArray(value);
557
+ }
558
+ function isPrimitive(value) {
559
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
560
+ }
561
+ function keyToToon(key) {
562
+ return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
563
+ }
564
+ function scalarToToon(value) {
565
+ if (value === null) return "null";
566
+ if (typeof value === "number" || typeof value === "boolean") {
567
+ return String(value);
568
+ }
569
+ const text = String(value);
570
+ if (text && !/^(true|false|null|-?\d+(?:\.\d+)?)$/i.test(text) && /^[A-Za-z0-9_./@-]+(?: [A-Za-z0-9_./@-]+)*$/.test(text)) {
571
+ return text;
572
+ }
573
+ return JSON.stringify(text);
574
+ }
575
+
118
576
  // src/client.ts
119
577
  var CALL_BEGIN_PATH = "call_begin";
120
578
  var CALL_END_PATH = "call_end";
579
+ var COMPRESS_PROMPT_PATH = "compress_prompt";
121
580
  var CHECK_USAGE_PATH = "customers/{customerId}/usage";
122
581
  var CREATE_CUSTOMER_PATH = "customers";
123
582
  var CHANGE_PLAN_PATH = "customers/{customerId}/change_plan";
@@ -129,7 +588,7 @@ var IDEMPOTENCY_HEADER = "idempotency-key";
129
588
  var SDK_HEADER = "x-usage-sdk";
130
589
  var USER_AGENT = "UsageTapClient";
131
590
  var CANONICAL_MEDIA_TYPE = "application/vnd.usagetap.v1+json";
132
- var SDK_VERSION = "0.9.0" ;
591
+ var SDK_VERSION = "1.0.0" ;
133
592
  var HAS_WINDOW = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
134
593
  var UsageTapClient = class {
135
594
  apiKey;
@@ -144,6 +603,8 @@ var UsageTapClient = class {
144
603
  metricFn;
145
604
  authHeader;
146
605
  autoIdempotency;
606
+ tokenCompanyApiKey;
607
+ tokenCompanyEndpoint;
147
608
  constructor(options) {
148
609
  if (!options) {
149
610
  throw new UsageTapError(
@@ -189,6 +650,8 @@ var UsageTapClient = class {
189
650
  this.metricFn = options.onUsageMetric;
190
651
  this.authHeader = options.useApiKeyHeader ? API_KEY_HEADER : AUTH_HEADER;
191
652
  this.autoIdempotency = options.autoIdempotency ?? true;
653
+ this.tokenCompanyApiKey = options.tokenCompanyApiKey;
654
+ this.tokenCompanyEndpoint = options.tokenCompanyEndpoint;
192
655
  }
193
656
  async beginCall(request, options = {}) {
194
657
  const idempotencyKey = request.idempotencyKey ?? request.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
@@ -211,6 +674,42 @@ var UsageTapClient = class {
211
674
  );
212
675
  return response;
213
676
  }
677
+ async promptCompress(request, options = {}) {
678
+ if (!request?.callId) {
679
+ throw new UsageTapError(
680
+ "USAGETAP_BAD_REQUEST",
681
+ "promptCompress requires callId"
682
+ );
683
+ }
684
+ const result = await compressPrompt({
685
+ input: request.input,
686
+ provider: request.provider,
687
+ tokenCompanyApiKey: this.tokenCompanyApiKey,
688
+ tokenCompanyEndpoint: this.tokenCompanyEndpoint,
689
+ fetchImpl: this.fetchImpl,
690
+ signal: options.signal
691
+ });
692
+ try {
693
+ await this.request(
694
+ COMPRESS_PROMPT_PATH,
695
+ {
696
+ callId: request.callId,
697
+ promptCompression: this.toPromptCompressionTelemetry(result)
698
+ },
699
+ options
700
+ );
701
+ return { ...result, callId: request.callId };
702
+ } catch (error) {
703
+ return {
704
+ ...createPromptCompressionFallback(
705
+ request.input,
706
+ request.provider ?? result.provider,
707
+ error
708
+ ),
709
+ callId: request.callId
710
+ };
711
+ }
712
+ }
214
713
  async endCall(request, options = {}) {
215
714
  if (!request?.callId) {
216
715
  throw new UsageTapError(
@@ -435,6 +934,20 @@ var UsageTapClient = class {
435
934
  }
436
935
  return handlerResult;
437
936
  }
937
+ toPromptCompressionTelemetry(result) {
938
+ return {
939
+ provider: result.provider,
940
+ originalCharacters: result.originalCharacters,
941
+ compressedCharacters: result.compressedCharacters,
942
+ savedCharacters: result.savedCharacters,
943
+ originalTokens: result.originalTokens,
944
+ compressedTokens: result.compressedTokens,
945
+ savedTokens: result.savedTokens,
946
+ tokenSavingsRatio: result.tokenSavingsRatio,
947
+ savingsRatio: result.savingsRatio,
948
+ techniques: result.techniques
949
+ };
950
+ }
438
951
  async request(path, payload, options) {
439
952
  const url = new URL(path, this.baseUrl).toString();
440
953
  const body = payload !== void 0 ? JSON.stringify(payload) : void 0;
@@ -1024,7 +1537,11 @@ async function finalizeCall(callState, usageTap, error, usage) {
1024
1537
 
1025
1538
  exports.UsageTapClient = UsageTapClient;
1026
1539
  exports.UsageTapError = UsageTapError;
1540
+ exports.compressPrompt = compressPrompt;
1541
+ exports.compressPromptHeuristic = compressPromptHeuristic;
1542
+ exports.compressPromptToon = compressPromptToon;
1027
1543
  exports.createIdempotencyKey = createIdempotencyKey;
1544
+ exports.estimatePromptTokens = estimatePromptTokens;
1028
1545
  exports.isUsageTapError = isUsageTapError;
1029
1546
  exports.wrapFetch = wrapFetch;
1030
1547
  //# sourceMappingURL=index.cjs.map