@usagetap/sdk 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1058 -863
  2. package/dist/adapters/anthropic.cjs +990 -24
  3. package/dist/adapters/anthropic.cjs.map +1 -1
  4. package/dist/adapters/anthropic.d.cts +45 -3
  5. package/dist/adapters/anthropic.d.ts +45 -3
  6. package/dist/adapters/anthropic.mjs +990 -25
  7. package/dist/adapters/anthropic.mjs.map +1 -1
  8. package/dist/adapters/openai.cjs +1164 -44
  9. package/dist/adapters/openai.cjs.map +1 -1
  10. package/dist/adapters/openai.d.cts +46 -3
  11. package/dist/adapters/openai.d.ts +46 -3
  12. package/dist/adapters/openai.mjs +1164 -45
  13. package/dist/adapters/openai.mjs.map +1 -1
  14. package/dist/adapters/openrouter.cjs +3899 -22
  15. package/dist/adapters/openrouter.cjs.map +1 -1
  16. package/dist/adapters/openrouter.d.cts +6 -3
  17. package/dist/adapters/openrouter.d.ts +6 -3
  18. package/dist/adapters/openrouter.mjs +3897 -23
  19. package/dist/adapters/openrouter.mjs.map +1 -1
  20. package/dist/anthropic/index.cjs +990 -24
  21. package/dist/anthropic/index.cjs.map +1 -1
  22. package/dist/anthropic/index.d.cts +2 -2
  23. package/dist/anthropic/index.d.ts +2 -2
  24. package/dist/anthropic/index.mjs +990 -25
  25. package/dist/anthropic/index.mjs.map +1 -1
  26. package/dist/client-CExQ8e1T.d.cts +1225 -0
  27. package/dist/client-CExQ8e1T.d.ts +1225 -0
  28. package/dist/express/index.cjs +421 -29
  29. package/dist/express/index.cjs.map +1 -1
  30. package/dist/express/index.d.cts +2 -2
  31. package/dist/express/index.d.ts +2 -2
  32. package/dist/express/index.mjs +421 -29
  33. package/dist/express/index.mjs.map +1 -1
  34. package/dist/index.cjs +680 -15
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +5 -3
  37. package/dist/index.d.ts +5 -3
  38. package/dist/index.mjs +680 -15
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/openai/index.cjs +1165 -45
  41. package/dist/openai/index.cjs.map +1 -1
  42. package/dist/openai/index.d.cts +2 -2
  43. package/dist/openai/index.d.ts +2 -2
  44. package/dist/openai/index.mjs +1165 -46
  45. package/dist/openai/index.mjs.map +1 -1
  46. package/dist/openrouter/index.cjs +1182 -47
  47. package/dist/openrouter/index.cjs.map +1 -1
  48. package/dist/openrouter/index.d.cts +3 -3
  49. package/dist/openrouter/index.d.ts +3 -3
  50. package/dist/openrouter/index.mjs +1180 -46
  51. package/dist/openrouter/index.mjs.map +1 -1
  52. package/dist/react/index.cjs +19 -1
  53. package/dist/react/index.cjs.map +1 -1
  54. package/dist/react/index.d.cts +17 -4
  55. package/dist/react/index.d.ts +17 -4
  56. package/dist/react/index.mjs +19 -1
  57. package/dist/react/index.mjs.map +1 -1
  58. package/package.json +84 -84
  59. package/dist/client-BD8O2J8Z.d.cts +0 -668
  60. package/dist/client-BD8O2J8Z.d.ts +0 -668
package/dist/index.mjs CHANGED
@@ -243,6 +243,11 @@ async function compressMessagesWithUsageTap(options) {
243
243
  aggressiveness,
244
244
  "UsageTap prompt message compression"
245
245
  );
246
+ if (options.latencyBudgetMs !== void 0 && (!Number.isFinite(options.latencyBudgetMs) || options.latencyBudgetMs < 0)) {
247
+ throw new Error(
248
+ "UsageTap prompt message compression latencyBudgetMs must be a non-negative number"
249
+ );
250
+ }
246
251
  const original = stableStringifyInput(options.input);
247
252
  const headers = {
248
253
  "content-type": "application/json"
@@ -257,7 +262,15 @@ async function compressMessagesWithUsageTap(options) {
257
262
  headers,
258
263
  body: JSON.stringify({
259
264
  ...cloneInputRecord(options.input),
260
- compression_settings: { aggressiveness }
265
+ compression_settings: {
266
+ aggressiveness,
267
+ ...options.mode === void 0 ? {} : { mode: options.mode },
268
+ ...options.latencyBudgetMs === void 0 ? {} : { latency_budget_ms: options.latencyBudgetMs },
269
+ ...options.compactEmptyUserMessages === void 0 ? {} : { compact_empty_user_messages: options.compactEmptyUserMessages },
270
+ ...options.compactDuplicateUserTextParts === void 0 ? {} : {
271
+ compact_duplicate_user_text_parts: options.compactDuplicateUserTextParts
272
+ }
273
+ }
261
274
  }),
262
275
  signal: options.signal
263
276
  }
@@ -753,10 +766,358 @@ function scalarToToon(value) {
753
766
  return JSON.stringify(text);
754
767
  }
755
768
 
769
+ // src/resources.ts
770
+ var CANONICAL_MEDIA_TYPE = "application/vnd.usagetap.v1+json";
771
+ var DEFAULT_GATEWAY_BASE_URL = "https://gateway.usagetap.com";
772
+ function normalizedBaseUrl(value) {
773
+ return `${value.replace(/\/+$/, "")}/`;
774
+ }
775
+ function errorMessage(payload, status) {
776
+ if (payload && typeof payload === "object") {
777
+ const record = payload;
778
+ const error = record.error;
779
+ if (error && typeof error === "object") {
780
+ const message2 = error.message;
781
+ if (typeof message2 === "string" && message2) return message2;
782
+ }
783
+ const message = record.message;
784
+ if (typeof message === "string" && message) return message;
785
+ }
786
+ return `UsageTap request failed with HTTP ${status}`;
787
+ }
788
+ function errorCode(status) {
789
+ if (status === 401 || status === 403) return "USAGETAP_AUTH_ERROR";
790
+ if (status === 429) return "USAGETAP_RATE_LIMITED";
791
+ if (status >= 500) return "USAGETAP_SERVER_ERROR";
792
+ return "USAGETAP_BAD_REQUEST";
793
+ }
794
+ var ResourceTransport = class {
795
+ baseUrl;
796
+ apiKey;
797
+ fetchImpl;
798
+ defaultHeaders;
799
+ sdkVersion;
800
+ constructor(baseUrl, config) {
801
+ this.baseUrl = normalizedBaseUrl(baseUrl);
802
+ this.apiKey = config.apiKey;
803
+ this.fetchImpl = config.fetchImpl;
804
+ this.defaultHeaders = config.headers ?? {};
805
+ this.sdkVersion = config.sdkVersion;
806
+ }
807
+ async request(request) {
808
+ const body = request.body === void 0 ? void 0 : JSON.stringify(request.body);
809
+ const headers = {
810
+ ...this.defaultHeaders,
811
+ accept: request.response === "data" ? CANONICAL_MEDIA_TYPE : request.response === "ndjson" ? "application/x-ndjson" : "application/json",
812
+ authorization: `Bearer ${this.apiKey}`,
813
+ "x-usage-sdk": `js/${this.sdkVersion}`,
814
+ ...body ? { "content-type": "application/json" } : {},
815
+ ...request.options?.idempotencyKey ? { "idempotency-key": request.options.idempotencyKey } : {},
816
+ ...request.options?.headers
817
+ };
818
+ let response;
819
+ try {
820
+ response = await this.fetchImpl(
821
+ new URL(request.path.replace(/^\/+/, ""), this.baseUrl),
822
+ {
823
+ method: request.method,
824
+ headers,
825
+ body,
826
+ signal: request.options?.signal
827
+ }
828
+ );
829
+ } catch (error) {
830
+ throw new UsageTapError(
831
+ "USAGETAP_NETWORK_ERROR",
832
+ "Failed to reach UsageTap",
833
+ { retryable: true, cause: error }
834
+ );
835
+ }
836
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-usage-correlation-id") ?? void 0;
837
+ const text = await response.text();
838
+ let payload;
839
+ if (text && request.response !== "ndjson") {
840
+ try {
841
+ payload = JSON.parse(text);
842
+ } catch (error) {
843
+ throw new UsageTapError(
844
+ "USAGETAP_INVALID_RESPONSE",
845
+ "UsageTap returned invalid JSON",
846
+ { status: response.status, correlationId: requestId, cause: error }
847
+ );
848
+ }
849
+ }
850
+ if (!response.ok) {
851
+ throw new UsageTapError(
852
+ errorCode(response.status),
853
+ errorMessage(payload, response.status),
854
+ {
855
+ status: response.status,
856
+ retryable: response.status === 429 || response.status >= 500,
857
+ correlationId: requestId,
858
+ details: payload && typeof payload === "object" ? payload : void 0
859
+ }
860
+ );
861
+ }
862
+ if (request.response === "ndjson") {
863
+ if (!text.trim()) return [];
864
+ try {
865
+ return text.trim().split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
866
+ } catch (error) {
867
+ throw new UsageTapError(
868
+ "USAGETAP_INVALID_RESPONSE",
869
+ "UsageTap returned invalid NDJSON",
870
+ { status: response.status, correlationId: requestId, cause: error }
871
+ );
872
+ }
873
+ }
874
+ if (request.response === "data") {
875
+ if (!payload || typeof payload !== "object" || !("data" in payload)) {
876
+ throw new UsageTapError(
877
+ "USAGETAP_INVALID_RESPONSE",
878
+ "UsageTap response missing data",
879
+ { status: response.status, correlationId: requestId }
880
+ );
881
+ }
882
+ return payload.data;
883
+ }
884
+ if (payload === void 0) {
885
+ throw new UsageTapError(
886
+ "USAGETAP_INVALID_RESPONSE",
887
+ "UsageTap response was empty",
888
+ { status: response.status, correlationId: requestId }
889
+ );
890
+ }
891
+ return payload;
892
+ }
893
+ };
894
+ function resourceId(value, keys, label) {
895
+ const id = typeof value === "string" ? value : keys.map((key) => value[key]).find((candidate) => Boolean(candidate?.trim()));
896
+ if (!id?.trim()) {
897
+ throw new UsageTapError(
898
+ "USAGETAP_BAD_REQUEST",
899
+ `${label} requires a non-empty ID`
900
+ );
901
+ }
902
+ return id.trim();
903
+ }
904
+ function terminalSummary(status) {
905
+ return status === "COMPLETE" || status === "FAILED";
906
+ }
907
+ function terminalGatewayBatch(status) {
908
+ return ["completed", "failed", "expired", "cancelled"].includes(status);
909
+ }
910
+ function validateWaitOptions(options) {
911
+ const pollIntervalMs = options.pollIntervalMs ?? 1500;
912
+ const timeoutMs = options.timeoutMs ?? 3 * 6e4;
913
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 0) {
914
+ throw new UsageTapError(
915
+ "USAGETAP_BAD_REQUEST",
916
+ "pollIntervalMs must be a non-negative number"
917
+ );
918
+ }
919
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
920
+ throw new UsageTapError(
921
+ "USAGETAP_BAD_REQUEST",
922
+ "timeoutMs must be a positive number"
923
+ );
924
+ }
925
+ return { pollIntervalMs, timeoutMs };
926
+ }
927
+ var SummarizationResource = class {
928
+ summaries;
929
+ batches;
930
+ profiles;
931
+ measurements;
932
+ transport;
933
+ constructor(config) {
934
+ this.transport = new ResourceTransport(config.apiBaseUrl, config);
935
+ this.summaries = {
936
+ create: (params, options) => this.transport.request({
937
+ method: "POST",
938
+ path: "/v1/compression/summaries",
939
+ body: params,
940
+ options,
941
+ response: "data"
942
+ }),
943
+ retrieve: (jobId, options) => this.transport.request({
944
+ method: "GET",
945
+ path: `/v1/compression/jobs/${encodeURIComponent(
946
+ resourceId(jobId, ["jobId"], "summaries.retrieve")
947
+ )}`,
948
+ options,
949
+ response: "data"
950
+ }),
951
+ wait: (job, options) => this.waitForSummary(job, options)
952
+ };
953
+ this.batches = {
954
+ create: (params, options) => this.transport.request({
955
+ method: "POST",
956
+ path: "/v1/compression/batches",
957
+ body: params,
958
+ options,
959
+ response: "data"
960
+ }),
961
+ retrieve: (batchId, options) => this.transport.request({
962
+ method: "GET",
963
+ path: `/v1/compression/batches/${encodeURIComponent(
964
+ resourceId(batchId, ["batchId"], "summarization.batches.retrieve")
965
+ )}`,
966
+ options,
967
+ response: "data"
968
+ }),
969
+ wait: (batch, options) => this.waitForBatch(batch, options)
970
+ };
971
+ this.profiles = {
972
+ retrieve: (profile, options) => this.transport.request({
973
+ method: "GET",
974
+ path: `/v1/compression/profiles/${encodeURIComponent(
975
+ resourceId(profile, [], "summarization.profiles.retrieve")
976
+ )}`,
977
+ options,
978
+ response: "data"
979
+ })
980
+ };
981
+ this.measurements = {
982
+ create: (params, options) => this.transport.request({
983
+ method: "POST",
984
+ path: "/v1/compression/measurements",
985
+ body: params,
986
+ options,
987
+ response: "data"
988
+ })
989
+ };
990
+ }
991
+ async waitForSummary(value, options = {}) {
992
+ const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
993
+ const deadline = Date.now() + timeoutMs;
994
+ let job = typeof value === "string" ? await this.summaries.retrieve(value, options) : value;
995
+ while (!terminalSummary(job.status)) {
996
+ if (Date.now() >= deadline) {
997
+ throw new UsageTapError(
998
+ "USAGETAP_RETRY_EXHAUSTED",
999
+ `Summarization job ${job.jobId} did not finish before timeout`,
1000
+ { retryable: true }
1001
+ );
1002
+ }
1003
+ await sleep(pollIntervalMs, options.signal);
1004
+ job = await this.summaries.retrieve(job.jobId, options);
1005
+ }
1006
+ return job;
1007
+ }
1008
+ async waitForBatch(value, options = {}) {
1009
+ const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
1010
+ const deadline = Date.now() + timeoutMs;
1011
+ let batch = typeof value === "string" ? await this.batches.retrieve(value, options) : value;
1012
+ while (!terminalSummary(batch.status)) {
1013
+ if (Date.now() >= deadline) {
1014
+ throw new UsageTapError(
1015
+ "USAGETAP_RETRY_EXHAUSTED",
1016
+ `Summarization batch ${batch.batchId} did not finish before timeout`,
1017
+ { retryable: true }
1018
+ );
1019
+ }
1020
+ await sleep(pollIntervalMs, options.signal);
1021
+ batch = await this.batches.retrieve(batch.batchId, options);
1022
+ }
1023
+ return batch;
1024
+ }
1025
+ };
1026
+ var GatewayResource = class {
1027
+ chat;
1028
+ models;
1029
+ batches;
1030
+ transport;
1031
+ idempotencyGenerator;
1032
+ constructor(config) {
1033
+ this.transport = new ResourceTransport(
1034
+ config.gatewayBaseUrl ?? DEFAULT_GATEWAY_BASE_URL,
1035
+ config
1036
+ );
1037
+ this.idempotencyGenerator = config.idempotencyGenerator ?? createIdempotencyKey;
1038
+ this.chat = {
1039
+ completions: {
1040
+ create: (params, options) => this.transport.request({
1041
+ method: "POST",
1042
+ path: "/v1/chat/completions",
1043
+ body: params,
1044
+ options,
1045
+ response: "json"
1046
+ })
1047
+ }
1048
+ };
1049
+ this.models = {
1050
+ list: (options) => this.transport.request({
1051
+ method: "GET",
1052
+ path: "/v1/models",
1053
+ options,
1054
+ response: "json"
1055
+ })
1056
+ };
1057
+ this.batches = {
1058
+ create: (params, options = {}) => this.transport.request({
1059
+ method: "POST",
1060
+ path: "/v1/batches",
1061
+ body: params,
1062
+ options: {
1063
+ ...options,
1064
+ idempotencyKey: options.idempotencyKey ?? this.idempotencyGenerator()
1065
+ },
1066
+ response: "json"
1067
+ }),
1068
+ retrieve: (batchId, options) => this.transport.request({
1069
+ method: "GET",
1070
+ path: `/v1/batches/${encodeURIComponent(
1071
+ resourceId(batchId, ["id"], "gateway.batches.retrieve")
1072
+ )}`,
1073
+ options,
1074
+ response: "json"
1075
+ }),
1076
+ wait: (batch, options) => this.waitForBatch(batch, options),
1077
+ cancel: (batchId, options) => this.transport.request({
1078
+ method: "POST",
1079
+ path: `/v1/batches/${encodeURIComponent(
1080
+ resourceId(batchId, ["id"], "gateway.batches.cancel")
1081
+ )}/cancel`,
1082
+ options,
1083
+ response: "json"
1084
+ }),
1085
+ results: (batchId, options) => this.transport.request({
1086
+ method: "GET",
1087
+ path: `/v1/batches/${encodeURIComponent(
1088
+ resourceId(batchId, ["id"], "gateway.batches.results")
1089
+ )}/results`,
1090
+ options,
1091
+ response: "ndjson"
1092
+ })
1093
+ };
1094
+ }
1095
+ async waitForBatch(value, options = {}) {
1096
+ const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
1097
+ const deadline = Date.now() + timeoutMs;
1098
+ let batch = typeof value === "string" ? await this.batches.retrieve(value, options) : value;
1099
+ while (!terminalGatewayBatch(batch.status)) {
1100
+ if (Date.now() >= deadline) {
1101
+ throw new UsageTapError(
1102
+ "USAGETAP_RETRY_EXHAUSTED",
1103
+ `Gateway batch ${batch.id} did not finish before timeout`,
1104
+ { retryable: true }
1105
+ );
1106
+ }
1107
+ await sleep(pollIntervalMs, options.signal);
1108
+ batch = await this.batches.retrieve(batch.id, options);
1109
+ }
1110
+ return batch;
1111
+ }
1112
+ };
1113
+
756
1114
  // src/client.ts
757
1115
  var CALL_BEGIN_PATH = "call_begin";
758
1116
  var CALL_END_PATH = "call_end";
759
1117
  var COMPRESS_PROMPT_PATH = "compress_prompt";
1118
+ var SAMPLES_PATH = "samples";
1119
+ var SAMPLING_SETTINGS_PATH = "sampling/settings";
1120
+ var SAMPLING_DECIDE_PATH = "sampling/decide";
760
1121
  var CHECK_USAGE_PATH = "customers/{customerId}/usage";
761
1122
  var CREATE_CUSTOMER_PATH = "customers";
762
1123
  var CHANGE_PLAN_PATH = "customers/{customerId}/change_plan";
@@ -767,11 +1128,16 @@ var CORRELATION_HEADER = "x-usage-correlation-id";
767
1128
  var IDEMPOTENCY_HEADER = "idempotency-key";
768
1129
  var SDK_HEADER = "x-usage-sdk";
769
1130
  var USER_AGENT = "UsageTapClient";
770
- var CANONICAL_MEDIA_TYPE = "application/vnd.usagetap.v1+json";
1131
+ var CANONICAL_MEDIA_TYPE2 = "application/vnd.usagetap.v1+json";
771
1132
  var DEFAULT_BASE_URL = "https://api.usagetap.com";
772
- var SDK_VERSION = "1.3.1" ;
1133
+ var DEFAULT_RUN_INACTIVITY_MS = 60 * 60 * 1e3;
1134
+ var SDK_VERSION = "1.4.0" ;
773
1135
  var HAS_WINDOW = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
774
1136
  var UsageTapClient = class {
1137
+ /** OpenAI-compatible chat, model, and native batch operations. */
1138
+ gateway;
1139
+ /** Published-profile context summarization operations. */
1140
+ summarization;
775
1141
  apiKey;
776
1142
  baseUrl;
777
1143
  fetchImpl;
@@ -796,6 +1162,11 @@ var UsageTapClient = class {
796
1162
  usageTapCompressionMessagesEndpoint;
797
1163
  usageTapCompressionModel;
798
1164
  usageTapCompressionAggressiveness;
1165
+ sampling;
1166
+ samplingSettingsCacheMs;
1167
+ circuitBreaker;
1168
+ circuitBreakerRuns = /* @__PURE__ */ new Map();
1169
+ samplingSettingsCache;
799
1170
  constructor(options = {}) {
800
1171
  const apiKey = options.apiKey?.trim() || readEnvironmentVariable("USAGETAP_API_KEY");
801
1172
  const baseUrl = options.baseUrl?.trim() || readEnvironmentVariable("USAGETAP_BASE_URL") || DEFAULT_BASE_URL;
@@ -818,10 +1189,21 @@ var UsageTapClient = class {
818
1189
  "A global fetch implementation was not found. Pass fetchImpl in UsageTapClientOptions."
819
1190
  );
820
1191
  }
821
- const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
822
- this.baseUrl = new URL(normalizedBaseUrl);
1192
+ const normalizedBaseUrl2 = normalizeBaseUrl(baseUrl);
1193
+ this.baseUrl = new URL(normalizedBaseUrl2);
823
1194
  this.apiKey = apiKey;
824
1195
  this.fetchImpl = wrapFetchImplementation(fetchCandidate, !options.fetchImpl);
1196
+ const resourceConfig = {
1197
+ apiKey,
1198
+ apiBaseUrl: normalizedBaseUrl2,
1199
+ gatewayBaseUrl: options.gatewayBaseUrl?.trim() || readEnvironmentVariable("USAGETAP_GATEWAY_URL"),
1200
+ fetchImpl: this.fetchImpl,
1201
+ headers: options.headers,
1202
+ sdkVersion: SDK_VERSION,
1203
+ idempotencyGenerator: options.idempotencyGenerator
1204
+ };
1205
+ this.gateway = new GatewayResource(resourceConfig);
1206
+ this.summarization = new SummarizationResource(resourceConfig);
825
1207
  this.defaultFeature = options.defaultFeature;
826
1208
  this.defaultTags = options.defaultTags?.length ? dedupeStrings(options.defaultTags) : void 0;
827
1209
  this.defaultHeaders = options.headers ? normalizeHeaderDictionary(options.headers) : {};
@@ -843,11 +1225,122 @@ var UsageTapClient = class {
843
1225
  this.usageTapCompressionMessagesEndpoint = options.usageTapCompressionMessagesEndpoint;
844
1226
  this.usageTapCompressionModel = options.usageTapCompressionModel;
845
1227
  this.usageTapCompressionAggressiveness = options.usageTapCompressionAggressiveness;
1228
+ this.sampling = options.sampling;
1229
+ this.samplingSettingsCacheMs = Number.isFinite(options.samplingSettingsCacheMs) ? Math.max(0, Number(options.samplingSettingsCacheMs)) : 5 * 60 * 1e3;
1230
+ if (options.circuitBreaker) {
1231
+ const maxCallsPerRun = options.circuitBreaker.maxCallsPerRun;
1232
+ if (!Number.isInteger(maxCallsPerRun) || maxCallsPerRun < 1) {
1233
+ throw new UsageTapError(
1234
+ "USAGETAP_BAD_REQUEST",
1235
+ "circuitBreaker.maxCallsPerRun must be a positive integer"
1236
+ );
1237
+ }
1238
+ const runInactivityMs = options.circuitBreaker.runInactivityMs ?? DEFAULT_RUN_INACTIVITY_MS;
1239
+ if (!Number.isFinite(runInactivityMs) || runInactivityMs < 1) {
1240
+ throw new UsageTapError(
1241
+ "USAGETAP_BAD_REQUEST",
1242
+ "circuitBreaker.runInactivityMs must be a positive number"
1243
+ );
1244
+ }
1245
+ this.circuitBreaker = {
1246
+ maxCallsPerRun,
1247
+ runInactivityMs
1248
+ };
1249
+ }
1250
+ }
1251
+ shouldSample(request, policy = this.sampling || void 0) {
1252
+ if (!policy) return false;
1253
+ const rate = Math.min(1, Math.max(0, Number(policy.rate) || 0));
1254
+ if (rate <= 0) return false;
1255
+ const customerId = request.customerId?.trim();
1256
+ if (customerId && policy.customers?.exclude?.includes(customerId)) return false;
1257
+ const feature = request.feature?.trim();
1258
+ if (feature && policy.features?.exclude?.includes(feature)) return false;
1259
+ const included = policy.features?.include?.filter(Boolean) ?? [];
1260
+ if (included.length > 0 && (!feature || !included.includes(feature))) return false;
1261
+ const minimum = Math.max(0, Math.round(policy.minInputTokens ?? 0));
1262
+ if (minimum > 0 && estimatePromptTokens(request.input) < minimum) return false;
1263
+ return (policy.random ?? Math.random)() < rate;
1264
+ }
1265
+ async getSamplingSettings(options = {}) {
1266
+ const now = Date.now();
1267
+ if (!options.forceRefresh && this.samplingSettingsCache && this.samplingSettingsCache.expiresAtMs > now) {
1268
+ return {
1269
+ result: { status: "ACCEPTED", code: "SAMPLING_SETTINGS_CACHED" },
1270
+ data: this.samplingSettingsCache.settings,
1271
+ correlationId: options.correlationId ?? "local-cache"
1272
+ };
1273
+ }
1274
+ const response = await this.requestGet(
1275
+ SAMPLING_SETTINGS_PATH,
1276
+ {
1277
+ signal: options.signal,
1278
+ headers: options.headers,
1279
+ retries: options.retries,
1280
+ correlationId: options.correlationId
1281
+ }
1282
+ );
1283
+ const serverCacheMs = Math.max(0, Number(response.data.cacheSeconds) || 0) * 1e3;
1284
+ const cacheMs = Math.min(this.samplingSettingsCacheMs, serverCacheMs);
1285
+ this.samplingSettingsCache = {
1286
+ settings: response.data,
1287
+ expiresAtMs: now + cacheMs
1288
+ };
1289
+ return response;
1290
+ }
1291
+ async shouldSampleAsync(request, policy) {
1292
+ if (policy) return this.shouldSample(request, policy);
1293
+ if (this.sampling === false) return false;
1294
+ if (this.sampling) return this.shouldSample(request, this.sampling);
1295
+ try {
1296
+ const settings = await this.getSamplingSettings();
1297
+ return this.shouldSample(request, settings.data);
1298
+ } catch {
1299
+ return false;
1300
+ }
1301
+ }
1302
+ async decideSample(request, options = {}) {
1303
+ const hasTokens = Number.isFinite(request.inputTokens) && Number(request.inputTokens) >= 0;
1304
+ const hasCharacters = Number.isFinite(request.inputCharacters) && Number(request.inputCharacters) >= 0;
1305
+ if (!hasTokens && !hasCharacters) {
1306
+ throw new UsageTapError(
1307
+ "USAGETAP_BAD_REQUEST",
1308
+ "decideSample requires inputTokens or inputCharacters"
1309
+ );
1310
+ }
1311
+ return this.request(
1312
+ SAMPLING_DECIDE_PATH,
1313
+ request,
1314
+ options
1315
+ );
1316
+ }
1317
+ async captureSample(request, options = {}) {
1318
+ if (!request || request.input === void 0) {
1319
+ throw new UsageTapError(
1320
+ "USAGETAP_BAD_REQUEST",
1321
+ "captureSample requires input"
1322
+ );
1323
+ }
1324
+ if (!request.provider?.trim()) {
1325
+ throw new UsageTapError(
1326
+ "USAGETAP_BAD_REQUEST",
1327
+ "captureSample requires provider"
1328
+ );
1329
+ }
1330
+ const sampleId = request.sampleId?.trim() || this.idempotencyGenerator();
1331
+ return this.request(
1332
+ SAMPLES_PATH,
1333
+ { ...request, sampleId },
1334
+ { ...options, idempotencyKey: sampleId }
1335
+ );
846
1336
  }
847
1337
  async beginCall(request, options = {}) {
848
1338
  const idempotencyKey = request.idempotencyKey ?? request.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
1339
+ this.reserveRunCall(request, idempotencyKey);
1340
+ const apiRequest = { ...request };
1341
+ delete apiRequest.runId;
849
1342
  const payload = {
850
- ...request,
1343
+ ...apiRequest,
851
1344
  feature: request.feature ?? this.defaultFeature,
852
1345
  tags: this.mergeTags(request.tags)
853
1346
  };
@@ -865,6 +1358,28 @@ var UsageTapClient = class {
865
1358
  );
866
1359
  return response;
867
1360
  }
1361
+ /**
1362
+ * Inspect a configured run circuit breaker without consuming another call.
1363
+ */
1364
+ canRunContinue(request) {
1365
+ const identity = this.resolveRunIdentity(request);
1366
+ if (!identity || !this.circuitBreaker) {
1367
+ throw new UsageTapError(
1368
+ "USAGETAP_BAD_REQUEST",
1369
+ "canRunContinue requires circuitBreaker configuration and a non-empty runId"
1370
+ );
1371
+ }
1372
+ this.expireInactiveRuns();
1373
+ const calls = this.circuitBreakerRuns.get(identity.key)?.calls ?? 0;
1374
+ return this.createCircuitBreakerDecision(identity.customerId, identity.runId, calls);
1375
+ }
1376
+ /**
1377
+ * Release local state after a workflow finishes. Returns true when state existed.
1378
+ */
1379
+ resetRun(request) {
1380
+ const identity = this.resolveRunIdentity(request);
1381
+ return identity ? this.circuitBreakerRuns.delete(identity.key) : false;
1382
+ }
868
1383
  async promptCompress(request, options = {}) {
869
1384
  if (!request?.callId) {
870
1385
  throw new UsageTapError(
@@ -962,6 +1477,10 @@ var UsageTapClient = class {
962
1477
  usageTapCompressionMessagesEndpoint: this.usageTapCompressionMessagesEndpoint,
963
1478
  aggressiveness: options.aggressiveness ?? this.aggressiveness,
964
1479
  usageTapCompressionAggressiveness: options.usageTapCompressionAggressiveness ?? this.usageTapCompressionAggressiveness,
1480
+ mode: options.mode,
1481
+ latencyBudgetMs: options.latencyBudgetMs,
1482
+ compactEmptyUserMessages: options.compactEmptyUserMessages,
1483
+ compactDuplicateUserTextParts: options.compactDuplicateUserTextParts,
965
1484
  fetchImpl: this.fetchImpl,
966
1485
  signal: options.signal,
967
1486
  failOpen: options.failOpen
@@ -1003,11 +1522,17 @@ var UsageTapClient = class {
1003
1522
  callId: request.callId,
1004
1523
  feature: feature ?? this.defaultFeature,
1005
1524
  tags: tags ?? this.defaultTags,
1525
+ providerUsed: request.providerUsed,
1006
1526
  modelUsed: request.modelUsed,
1527
+ reasoningEffort: request.reasoningEffort,
1528
+ reasoningEffortSource: request.reasoningEffortSource,
1529
+ reasoningMode: request.reasoningMode,
1530
+ reasoningBudgetTokens: request.reasoningBudgetTokens,
1007
1531
  metrics: {
1008
1532
  inputTokens: request.inputTokens,
1009
1533
  responseTokens: request.responseTokens,
1010
1534
  cachedInputTokens: request.cachedInputTokens,
1535
+ cacheWriteInputTokens: request.cacheWriteInputTokens,
1011
1536
  reasoningTokens: request.reasoningTokens,
1012
1537
  searches: request.searches,
1013
1538
  audioSeconds: request.audioSeconds,
@@ -1115,6 +1640,15 @@ var UsageTapClient = class {
1115
1640
  meterSlot: request.meterSlot,
1116
1641
  amount: request.amount
1117
1642
  };
1643
+ if (request.customerUserId) {
1644
+ payload.customerUserId = request.customerUserId;
1645
+ }
1646
+ if (request.customerUserName) {
1647
+ payload.customerUserName = request.customerUserName;
1648
+ }
1649
+ if (request.customerUserEmail) {
1650
+ payload.customerUserEmail = request.customerUserEmail;
1651
+ }
1118
1652
  if (request.feature) {
1119
1653
  payload.feature = request.feature;
1120
1654
  }
@@ -1151,6 +1685,11 @@ var UsageTapClient = class {
1151
1685
  const beginPayload = idempotencyKey ? { ...beginRequest, idempotencyKey, idempotency: idempotencyKey } : { ...beginRequest };
1152
1686
  const beginResponse = await this.beginCall(beginPayload, options);
1153
1687
  let usage = {};
1688
+ const pricingMode = beginResponse.data.pricingMode ?? beginRequest.pricingMode ?? (beginRequest.batch === true ? "batch" : beginRequest.batch === false ? "standard" : void 0);
1689
+ if (pricingMode) {
1690
+ usage.pricingMode = pricingMode;
1691
+ usage.batch = pricingMode === "batch";
1692
+ }
1154
1693
  const initialStripeCustomerId = typeof beginResponse.data.stripeCustomerId === "string" ? beginResponse.data.stripeCustomerId : typeof beginRequest.stripeCustomerId === "string" ? beginRequest.stripeCustomerId : void 0;
1155
1694
  if (initialStripeCustomerId) {
1156
1695
  usage = { ...usage, stripeCustomerId: initialStripeCustomerId };
@@ -1218,17 +1757,86 @@ var UsageTapClient = class {
1218
1757
  toPromptCompressionTelemetry(result) {
1219
1758
  return {
1220
1759
  provider: result.provider,
1221
- originalCharacters: result.originalCharacters,
1222
- compressedCharacters: result.compressedCharacters,
1223
- savedCharacters: result.savedCharacters,
1224
1760
  originalTokens: result.originalTokens,
1225
1761
  compressedTokens: result.compressedTokens,
1226
1762
  savedTokens: result.savedTokens,
1227
1763
  tokenSavingsRatio: result.tokenSavingsRatio,
1228
- savingsRatio: result.savingsRatio,
1229
1764
  techniques: result.techniques
1230
1765
  };
1231
1766
  }
1767
+ reserveRunCall(request, idempotencyKey) {
1768
+ const identity = this.resolveRunIdentity(request);
1769
+ if (!identity || !this.circuitBreaker) return;
1770
+ this.expireInactiveRuns();
1771
+ const now = Date.now();
1772
+ const state = this.circuitBreakerRuns.get(identity.key) ?? {
1773
+ calls: 0,
1774
+ reservationKeys: /* @__PURE__ */ new Set(),
1775
+ lastSeenAtMs: now
1776
+ };
1777
+ const reservationKey = idempotencyKey ?? this.idempotencyGenerator();
1778
+ state.lastSeenAtMs = now;
1779
+ if (state.reservationKeys.has(reservationKey)) {
1780
+ this.circuitBreakerRuns.set(identity.key, state);
1781
+ return;
1782
+ }
1783
+ const decision = this.createCircuitBreakerDecision(
1784
+ identity.customerId,
1785
+ identity.runId,
1786
+ state.calls
1787
+ );
1788
+ if (!decision.allowed) {
1789
+ throw new UsageTapError(
1790
+ "USAGETAP_CIRCUIT_OPEN",
1791
+ `Run ${identity.runId} reached its ${decision.limit}-call circuit-breaker limit`,
1792
+ {
1793
+ details: {
1794
+ reason: decision.reason,
1795
+ customerId: identity.customerId,
1796
+ runId: identity.runId,
1797
+ calls: decision.calls,
1798
+ limit: decision.limit,
1799
+ remaining: decision.remaining
1800
+ }
1801
+ }
1802
+ );
1803
+ }
1804
+ state.calls += 1;
1805
+ state.reservationKeys.add(reservationKey);
1806
+ this.circuitBreakerRuns.set(identity.key, state);
1807
+ }
1808
+ resolveRunIdentity(request) {
1809
+ const customerId = request.customerId?.trim();
1810
+ const runId = request.runId?.trim();
1811
+ if (!customerId || !runId) return void 0;
1812
+ return {
1813
+ key: `${customerId}\0${runId}`,
1814
+ customerId,
1815
+ runId
1816
+ };
1817
+ }
1818
+ createCircuitBreakerDecision(customerId, runId, calls) {
1819
+ const limit = this.circuitBreaker?.maxCallsPerRun ?? 0;
1820
+ const allowed = calls < limit;
1821
+ return {
1822
+ allowed,
1823
+ ...allowed ? {} : { reason: "max_calls_per_run" },
1824
+ customerId,
1825
+ runId,
1826
+ calls,
1827
+ limit,
1828
+ remaining: Math.max(0, limit - calls)
1829
+ };
1830
+ }
1831
+ expireInactiveRuns() {
1832
+ if (!this.circuitBreaker || this.circuitBreakerRuns.size === 0) return;
1833
+ const expiredBefore = Date.now() - this.circuitBreaker.runInactivityMs;
1834
+ for (const [key, state] of this.circuitBreakerRuns) {
1835
+ if (state.lastSeenAtMs < expiredBefore) {
1836
+ this.circuitBreakerRuns.delete(key);
1837
+ }
1838
+ }
1839
+ }
1232
1840
  async request(path, payload, options) {
1233
1841
  const url = new URL(path, this.baseUrl).toString();
1234
1842
  const body = payload !== void 0 ? JSON.stringify(payload) : void 0;
@@ -1416,7 +2024,7 @@ var UsageTapClient = class {
1416
2024
  ...this.defaultHeaders,
1417
2025
  [SDK_HEADER]: `js/${SDK_VERSION}`,
1418
2026
  "content-type": "application/json",
1419
- accept: CANONICAL_MEDIA_TYPE
2027
+ accept: CANONICAL_MEDIA_TYPE2
1420
2028
  };
1421
2029
  if (!HAS_WINDOW) {
1422
2030
  headers["user-agent"] = `${USER_AGENT}/${SDK_VERSION}`;
@@ -1604,7 +2212,8 @@ function wrapFetch(usageTap, options) {
1604
2212
  defaultContext,
1605
2213
  baseFetch = globalThis.fetch,
1606
2214
  autoIdempotency = true,
1607
- isOpenAIEndpoint = defaultIsOpenAIEndpoint
2215
+ isOpenAIEndpoint = defaultIsOpenAIEndpoint,
2216
+ provider = "openai"
1608
2217
  } = options;
1609
2218
  return async function wrappedFetch(input, init) {
1610
2219
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -1626,6 +2235,7 @@ function wrapFetch(usageTap, options) {
1626
2235
  const customerIdHeader = headers.get("x-usagetap-customer-id");
1627
2236
  const featureHeader = headers.get("x-usagetap-feature");
1628
2237
  const tagsHeader = headers.get("x-usagetap-tags");
2238
+ const runIdHeader = headers.get("x-usagetap-run-id");
1629
2239
  if (customerIdHeader) {
1630
2240
  contextOverride.customerId = customerIdHeader;
1631
2241
  }
@@ -1638,6 +2248,9 @@ function wrapFetch(usageTap, options) {
1638
2248
  contextOverride.tags = tags;
1639
2249
  }
1640
2250
  }
2251
+ if (runIdHeader) {
2252
+ contextOverride.runId = runIdHeader;
2253
+ }
1641
2254
  }
1642
2255
  } catch {
1643
2256
  return baseFetch(input, init);
@@ -1653,10 +2266,25 @@ function wrapFetch(usageTap, options) {
1653
2266
  callState = {
1654
2267
  callId: beginResponse.data.callId,
1655
2268
  correlationId: beginResponse.correlationId,
1656
- usage: {},
2269
+ usage: {
2270
+ ...executionMetadataFromOpenAIRequest(body, provider),
2271
+ ...beginResponse.data.pricingMode ? {
2272
+ pricingMode: beginResponse.data.pricingMode,
2273
+ batch: beginResponse.data.pricingMode === "batch"
2274
+ } : context.pricingMode ? {
2275
+ pricingMode: context.pricingMode,
2276
+ batch: context.pricingMode === "batch"
2277
+ } : typeof context.batch === "boolean" ? {
2278
+ batch: context.batch,
2279
+ pricingMode: context.batch ? "batch" : "standard"
2280
+ } : {}
2281
+ },
1657
2282
  finalized: false
1658
2283
  };
1659
2284
  } catch (error) {
2285
+ if (isUsageTapError(error) && error.code === "USAGETAP_CIRCUIT_OPEN") {
2286
+ throw error;
2287
+ }
1660
2288
  console.error("[wrapFetch] Failed to begin call:", error);
1661
2289
  return baseFetch(input, init);
1662
2290
  }
@@ -1697,6 +2325,9 @@ async function wrapNonStreamingResponse(response, callState, usageTap, requestBo
1697
2325
  if (isJsonRecord(usageBlock)) {
1698
2326
  const usageRecord = usageBlock;
1699
2327
  const promptDetails = isJsonRecord(usageRecord.prompt_tokens_details) ? usageRecord.prompt_tokens_details : void 0;
2328
+ const completionDetails = isJsonRecord(
2329
+ usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details
2330
+ ) ? usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details : void 0;
1700
2331
  const promptTokens = readNumber(usageRecord.prompt_tokens ?? usageRecord.input_tokens);
1701
2332
  if (promptTokens !== void 0) {
1702
2333
  usage.inputTokens = promptTokens;
@@ -1711,7 +2342,9 @@ async function wrapNonStreamingResponse(response, callState, usageTap, requestBo
1711
2342
  if (cachedTokens !== void 0) {
1712
2343
  usage.cachedInputTokens = cachedTokens;
1713
2344
  }
1714
- const reasoningTokens = readNumber(usageRecord.reasoning_tokens);
2345
+ const reasoningTokens = readNumber(
2346
+ usageRecord.reasoning_tokens ?? completionDetails?.reasoning_tokens
2347
+ );
1715
2348
  if (reasoningTokens !== void 0) {
1716
2349
  usage.reasoningTokens = reasoningTokens;
1717
2350
  }
@@ -1720,6 +2353,14 @@ async function wrapNonStreamingResponse(response, callState, usageTap, requestBo
1720
2353
  if (modelFromResponse) {
1721
2354
  usage.modelUsed = modelFromResponse;
1722
2355
  }
2356
+ const reasoning = isJsonRecord(parsed.reasoning) ? parsed.reasoning : void 0;
2357
+ const responseEffort = normalizeReasoningEffort(reasoning?.effort);
2358
+ if (responseEffort) {
2359
+ usage.reasoningEffort = responseEffort;
2360
+ usage.reasoningEffortSource = "provider_response";
2361
+ }
2362
+ const responseMode = readString(reasoning?.type ?? reasoning?.mode);
2363
+ if (responseMode) usage.reasoningMode = responseMode;
1723
2364
  }
1724
2365
  if (!usage.modelUsed && requestBody) {
1725
2366
  const requestModel = readString(requestBody.model);
@@ -1767,6 +2408,9 @@ function wrapStreamingResponse(response, callState, usageTap) {
1767
2408
  if (isJsonRecord(usageBlock)) {
1768
2409
  const usageRecord = usageBlock;
1769
2410
  const promptDetails = isJsonRecord(usageRecord.prompt_tokens_details) ? usageRecord.prompt_tokens_details : void 0;
2411
+ const completionDetails = isJsonRecord(
2412
+ usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details
2413
+ ) ? usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details : void 0;
1770
2414
  const promptTokens = readNumber(usageRecord.prompt_tokens ?? usageRecord.input_tokens);
1771
2415
  if (promptTokens !== void 0) {
1772
2416
  accumulatedUsage.inputTokens = promptTokens;
@@ -1781,7 +2425,9 @@ function wrapStreamingResponse(response, callState, usageTap) {
1781
2425
  if (cachedTokens !== void 0) {
1782
2426
  accumulatedUsage.cachedInputTokens = cachedTokens;
1783
2427
  }
1784
- const reasoningTokens = readNumber(usageRecord.reasoning_tokens);
2428
+ const reasoningTokens = readNumber(
2429
+ usageRecord.reasoning_tokens ?? completionDetails?.reasoning_tokens
2430
+ );
1785
2431
  if (reasoningTokens !== void 0) {
1786
2432
  accumulatedUsage.reasoningTokens = reasoningTokens;
1787
2433
  }
@@ -1806,6 +2452,25 @@ function wrapStreamingResponse(response, callState, usageTap) {
1806
2452
  headers: response.headers
1807
2453
  });
1808
2454
  }
2455
+ function normalizeReasoningEffort(value) {
2456
+ return value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max" ? value : void 0;
2457
+ }
2458
+ function executionMetadataFromOpenAIRequest(body, provider) {
2459
+ const reasoning = isJsonRecord(body?.reasoning) ? body.reasoning : void 0;
2460
+ const effort = normalizeReasoningEffort(
2461
+ body?.reasoning_effort ?? reasoning?.effort ?? body?.thinking_level
2462
+ );
2463
+ const mode = readString(reasoning?.type ?? reasoning?.mode);
2464
+ const budget = readNumber(
2465
+ reasoning?.budget_tokens ?? body?.thinking_budget ?? body?.thinking_budget_tokens
2466
+ );
2467
+ return {
2468
+ providerUsed: provider,
2469
+ ...effort ? { reasoningEffort: effort, reasoningEffortSource: "provider_request" } : {},
2470
+ ...mode ? { reasoningMode: mode } : {},
2471
+ ...budget !== void 0 ? { reasoningBudgetTokens: budget } : {}
2472
+ };
2473
+ }
1809
2474
  async function finalizeCall(callState, usageTap, error, usage) {
1810
2475
  if (callState.finalized) return;
1811
2476
  callState.finalized = true;