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