@usagetap/sdk 1.3.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +372 -39
  2. package/dist/adapters/anthropic.cjs +995 -69
  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 +995 -70
  7. package/dist/adapters/anthropic.mjs.map +1 -1
  8. package/dist/adapters/openai.cjs +1208 -106
  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 +1208 -107
  13. package/dist/adapters/openai.mjs.map +1 -1
  14. package/dist/adapters/openrouter.cjs +3912 -53
  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 +3910 -54
  19. package/dist/adapters/openrouter.mjs.map +1 -1
  20. package/dist/anthropic/index.cjs +995 -69
  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 +995 -70
  25. package/dist/anthropic/index.mjs.map +1 -1
  26. package/dist/client-C0UiaqVB.d.cts +1305 -0
  27. package/dist/client-C0UiaqVB.d.ts +1305 -0
  28. package/dist/express/index.cjs +399 -64
  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 +399 -64
  33. package/dist/express/index.mjs.map +1 -1
  34. package/dist/index.cjs +1044 -163
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +16 -5
  37. package/dist/index.d.ts +16 -5
  38. package/dist/index.mjs +1044 -163
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/openai/index.cjs +1209 -107
  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 +1209 -108
  45. package/dist/openai/index.mjs.map +1 -1
  46. package/dist/openrouter/index.cjs +1226 -109
  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 +1224 -108
  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 +2 -2
  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,369 @@ 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
+ /** Buffered OpenAI Responses-compatible requests. */
1031
+ responses;
1032
+ models;
1033
+ batches;
1034
+ transport;
1035
+ idempotencyGenerator;
1036
+ constructor(config) {
1037
+ this.transport = new ResourceTransport(
1038
+ config.gatewayBaseUrl ?? DEFAULT_GATEWAY_BASE_URL,
1039
+ config
1040
+ );
1041
+ this.idempotencyGenerator = config.idempotencyGenerator ?? createIdempotencyKey;
1042
+ this.chat = {
1043
+ completions: {
1044
+ create: (params, options) => this.transport.request({
1045
+ method: "POST",
1046
+ path: "/v1/chat/completions",
1047
+ body: params,
1048
+ options,
1049
+ response: "json"
1050
+ })
1051
+ }
1052
+ };
1053
+ this.responses = {
1054
+ create: (params, options) => this.transport.request({
1055
+ method: "POST",
1056
+ path: "/v1/responses",
1057
+ body: params,
1058
+ options,
1059
+ response: "json"
1060
+ })
1061
+ };
1062
+ this.models = {
1063
+ list: (options) => this.transport.request({
1064
+ method: "GET",
1065
+ path: "/v1/models",
1066
+ options,
1067
+ response: "json"
1068
+ })
1069
+ };
1070
+ this.batches = {
1071
+ create: (params, options = {}) => this.transport.request({
1072
+ method: "POST",
1073
+ path: "/v1/batches",
1074
+ body: params,
1075
+ options: {
1076
+ ...options,
1077
+ idempotencyKey: options.idempotencyKey ?? this.idempotencyGenerator()
1078
+ },
1079
+ response: "json"
1080
+ }),
1081
+ retrieve: (batchId, options) => this.transport.request({
1082
+ method: "GET",
1083
+ path: `/v1/batches/${encodeURIComponent(
1084
+ resourceId(batchId, ["id"], "gateway.batches.retrieve")
1085
+ )}`,
1086
+ options,
1087
+ response: "json"
1088
+ }),
1089
+ wait: (batch, options) => this.waitForBatch(batch, options),
1090
+ cancel: (batchId, options) => this.transport.request({
1091
+ method: "POST",
1092
+ path: `/v1/batches/${encodeURIComponent(
1093
+ resourceId(batchId, ["id"], "gateway.batches.cancel")
1094
+ )}/cancel`,
1095
+ options,
1096
+ response: "json"
1097
+ }),
1098
+ results: (batchId, options) => this.transport.request({
1099
+ method: "GET",
1100
+ path: `/v1/batches/${encodeURIComponent(
1101
+ resourceId(batchId, ["id"], "gateway.batches.results")
1102
+ )}/results`,
1103
+ options,
1104
+ response: "ndjson"
1105
+ })
1106
+ };
1107
+ }
1108
+ async waitForBatch(value, options = {}) {
1109
+ const { pollIntervalMs, timeoutMs } = validateWaitOptions(options);
1110
+ const deadline = Date.now() + timeoutMs;
1111
+ let batch = typeof value === "string" ? await this.batches.retrieve(value, options) : value;
1112
+ while (!terminalGatewayBatch(batch.status)) {
1113
+ if (Date.now() >= deadline) {
1114
+ throw new UsageTapError(
1115
+ "USAGETAP_RETRY_EXHAUSTED",
1116
+ `Gateway batch ${batch.id} did not finish before timeout`,
1117
+ { retryable: true }
1118
+ );
1119
+ }
1120
+ await sleep(pollIntervalMs, options.signal);
1121
+ batch = await this.batches.retrieve(batch.id, options);
1122
+ }
1123
+ return batch;
1124
+ }
1125
+ };
1126
+
758
1127
  // src/client.ts
759
1128
  var CALL_BEGIN_PATH = "call_begin";
760
1129
  var CALL_END_PATH = "call_end";
761
1130
  var COMPRESS_PROMPT_PATH = "compress_prompt";
1131
+ var SAMPLES_PATH = "samples";
1132
+ var SAMPLING_SETTINGS_PATH = "sampling/settings";
1133
+ var SAMPLING_DECIDE_PATH = "sampling/decide";
762
1134
  var CHECK_USAGE_PATH = "customers/{customerId}/usage";
763
1135
  var CREATE_CUSTOMER_PATH = "customers";
764
1136
  var CHANGE_PLAN_PATH = "customers/{customerId}/change_plan";
@@ -769,11 +1141,16 @@ var CORRELATION_HEADER = "x-usage-correlation-id";
769
1141
  var IDEMPOTENCY_HEADER = "idempotency-key";
770
1142
  var SDK_HEADER = "x-usage-sdk";
771
1143
  var USER_AGENT = "UsageTapClient";
772
- var CANONICAL_MEDIA_TYPE = "application/vnd.usagetap.v1+json";
1144
+ var CANONICAL_MEDIA_TYPE2 = "application/vnd.usagetap.v1+json";
773
1145
  var DEFAULT_BASE_URL = "https://api.usagetap.com";
774
- var SDK_VERSION = "1.3.2" ;
1146
+ var DEFAULT_RUN_INACTIVITY_MS = 60 * 60 * 1e3;
1147
+ var SDK_VERSION = "1.7.0" ;
775
1148
  var HAS_WINDOW = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
776
1149
  var UsageTapClient = class {
1150
+ /** OpenAI-compatible chat, model, and native batch operations. */
1151
+ gateway;
1152
+ /** Published-profile context summarization operations. */
1153
+ summarization;
777
1154
  apiKey;
778
1155
  baseUrl;
779
1156
  fetchImpl;
@@ -798,6 +1175,11 @@ var UsageTapClient = class {
798
1175
  usageTapCompressionMessagesEndpoint;
799
1176
  usageTapCompressionModel;
800
1177
  usageTapCompressionAggressiveness;
1178
+ sampling;
1179
+ samplingSettingsCacheMs;
1180
+ circuitBreaker;
1181
+ circuitBreakerRuns = /* @__PURE__ */ new Map();
1182
+ samplingSettingsCache;
801
1183
  constructor(options = {}) {
802
1184
  const apiKey = options.apiKey?.trim() || readEnvironmentVariable("USAGETAP_API_KEY");
803
1185
  const baseUrl = options.baseUrl?.trim() || readEnvironmentVariable("USAGETAP_BASE_URL") || DEFAULT_BASE_URL;
@@ -820,10 +1202,21 @@ var UsageTapClient = class {
820
1202
  "A global fetch implementation was not found. Pass fetchImpl in UsageTapClientOptions."
821
1203
  );
822
1204
  }
823
- const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
824
- this.baseUrl = new URL(normalizedBaseUrl);
1205
+ const normalizedBaseUrl2 = normalizeBaseUrl(baseUrl);
1206
+ this.baseUrl = new URL(normalizedBaseUrl2);
825
1207
  this.apiKey = apiKey;
826
1208
  this.fetchImpl = wrapFetchImplementation(fetchCandidate, !options.fetchImpl);
1209
+ const resourceConfig = {
1210
+ apiKey,
1211
+ apiBaseUrl: normalizedBaseUrl2,
1212
+ gatewayBaseUrl: options.gatewayBaseUrl?.trim() || readEnvironmentVariable("USAGETAP_GATEWAY_URL"),
1213
+ fetchImpl: this.fetchImpl,
1214
+ headers: options.headers,
1215
+ sdkVersion: SDK_VERSION,
1216
+ idempotencyGenerator: options.idempotencyGenerator
1217
+ };
1218
+ this.gateway = new GatewayResource(resourceConfig);
1219
+ this.summarization = new SummarizationResource(resourceConfig);
827
1220
  this.defaultFeature = options.defaultFeature;
828
1221
  this.defaultTags = options.defaultTags?.length ? dedupeStrings(options.defaultTags) : void 0;
829
1222
  this.defaultHeaders = options.headers ? normalizeHeaderDictionary(options.headers) : {};
@@ -845,11 +1238,122 @@ var UsageTapClient = class {
845
1238
  this.usageTapCompressionMessagesEndpoint = options.usageTapCompressionMessagesEndpoint;
846
1239
  this.usageTapCompressionModel = options.usageTapCompressionModel;
847
1240
  this.usageTapCompressionAggressiveness = options.usageTapCompressionAggressiveness;
1241
+ this.sampling = options.sampling;
1242
+ this.samplingSettingsCacheMs = Number.isFinite(options.samplingSettingsCacheMs) ? Math.max(0, Number(options.samplingSettingsCacheMs)) : 5 * 60 * 1e3;
1243
+ if (options.circuitBreaker) {
1244
+ const maxCallsPerRun = options.circuitBreaker.maxCallsPerRun;
1245
+ if (!Number.isInteger(maxCallsPerRun) || maxCallsPerRun < 1) {
1246
+ throw new UsageTapError(
1247
+ "USAGETAP_BAD_REQUEST",
1248
+ "circuitBreaker.maxCallsPerRun must be a positive integer"
1249
+ );
1250
+ }
1251
+ const runInactivityMs = options.circuitBreaker.runInactivityMs ?? DEFAULT_RUN_INACTIVITY_MS;
1252
+ if (!Number.isFinite(runInactivityMs) || runInactivityMs < 1) {
1253
+ throw new UsageTapError(
1254
+ "USAGETAP_BAD_REQUEST",
1255
+ "circuitBreaker.runInactivityMs must be a positive number"
1256
+ );
1257
+ }
1258
+ this.circuitBreaker = {
1259
+ maxCallsPerRun,
1260
+ runInactivityMs
1261
+ };
1262
+ }
1263
+ }
1264
+ shouldSample(request, policy = this.sampling || void 0) {
1265
+ if (!policy) return false;
1266
+ const rate = Math.min(1, Math.max(0, Number(policy.rate) || 0));
1267
+ if (rate <= 0) return false;
1268
+ const customerId = request.customerId?.trim();
1269
+ if (customerId && policy.customers?.exclude?.includes(customerId)) return false;
1270
+ const feature = request.feature?.trim();
1271
+ if (feature && policy.features?.exclude?.includes(feature)) return false;
1272
+ const included = policy.features?.include?.filter(Boolean) ?? [];
1273
+ if (included.length > 0 && (!feature || !included.includes(feature))) return false;
1274
+ const minimum = Math.max(0, Math.round(policy.minInputTokens ?? 0));
1275
+ if (minimum > 0 && estimatePromptTokens(request.input) < minimum) return false;
1276
+ return (policy.random ?? Math.random)() < rate;
1277
+ }
1278
+ async getSamplingSettings(options = {}) {
1279
+ const now = Date.now();
1280
+ if (!options.forceRefresh && this.samplingSettingsCache && this.samplingSettingsCache.expiresAtMs > now) {
1281
+ return {
1282
+ result: { status: "ACCEPTED", code: "SAMPLING_SETTINGS_CACHED" },
1283
+ data: this.samplingSettingsCache.settings,
1284
+ correlationId: options.correlationId ?? "local-cache"
1285
+ };
1286
+ }
1287
+ const response = await this.requestGet(
1288
+ SAMPLING_SETTINGS_PATH,
1289
+ {
1290
+ signal: options.signal,
1291
+ headers: options.headers,
1292
+ retries: options.retries,
1293
+ correlationId: options.correlationId
1294
+ }
1295
+ );
1296
+ const serverCacheMs = Math.max(0, Number(response.data.cacheSeconds) || 0) * 1e3;
1297
+ const cacheMs = Math.min(this.samplingSettingsCacheMs, serverCacheMs);
1298
+ this.samplingSettingsCache = {
1299
+ settings: response.data,
1300
+ expiresAtMs: now + cacheMs
1301
+ };
1302
+ return response;
1303
+ }
1304
+ async shouldSampleAsync(request, policy) {
1305
+ if (policy) return this.shouldSample(request, policy);
1306
+ if (this.sampling === false) return false;
1307
+ if (this.sampling) return this.shouldSample(request, this.sampling);
1308
+ try {
1309
+ const settings = await this.getSamplingSettings();
1310
+ return this.shouldSample(request, settings.data);
1311
+ } catch {
1312
+ return false;
1313
+ }
1314
+ }
1315
+ async decideSample(request, options = {}) {
1316
+ const hasTokens = Number.isFinite(request.inputTokens) && Number(request.inputTokens) >= 0;
1317
+ const hasCharacters = Number.isFinite(request.inputCharacters) && Number(request.inputCharacters) >= 0;
1318
+ if (!hasTokens && !hasCharacters) {
1319
+ throw new UsageTapError(
1320
+ "USAGETAP_BAD_REQUEST",
1321
+ "decideSample requires inputTokens or inputCharacters"
1322
+ );
1323
+ }
1324
+ return this.request(
1325
+ SAMPLING_DECIDE_PATH,
1326
+ request,
1327
+ options
1328
+ );
1329
+ }
1330
+ async captureSample(request, options = {}) {
1331
+ if (!request || request.input === void 0) {
1332
+ throw new UsageTapError(
1333
+ "USAGETAP_BAD_REQUEST",
1334
+ "captureSample requires input"
1335
+ );
1336
+ }
1337
+ if (!request.provider?.trim()) {
1338
+ throw new UsageTapError(
1339
+ "USAGETAP_BAD_REQUEST",
1340
+ "captureSample requires provider"
1341
+ );
1342
+ }
1343
+ const sampleId = request.sampleId?.trim() || this.idempotencyGenerator();
1344
+ return this.request(
1345
+ SAMPLES_PATH,
1346
+ { ...request, sampleId },
1347
+ { ...options, idempotencyKey: sampleId }
1348
+ );
848
1349
  }
849
1350
  async beginCall(request, options = {}) {
850
1351
  const idempotencyKey = request.idempotencyKey ?? request.idempotency ?? (this.autoIdempotency ? this.idempotencyGenerator() : void 0);
1352
+ this.reserveRunCall(request, idempotencyKey);
1353
+ const apiRequest = { ...request };
1354
+ delete apiRequest.runId;
851
1355
  const payload = {
852
- ...request,
1356
+ ...apiRequest,
853
1357
  feature: request.feature ?? this.defaultFeature,
854
1358
  tags: this.mergeTags(request.tags)
855
1359
  };
@@ -867,6 +1371,28 @@ var UsageTapClient = class {
867
1371
  );
868
1372
  return response;
869
1373
  }
1374
+ /**
1375
+ * Inspect a configured run circuit breaker without consuming another call.
1376
+ */
1377
+ canRunContinue(request) {
1378
+ const identity = this.resolveRunIdentity(request);
1379
+ if (!identity || !this.circuitBreaker) {
1380
+ throw new UsageTapError(
1381
+ "USAGETAP_BAD_REQUEST",
1382
+ "canRunContinue requires circuitBreaker configuration and a non-empty runId"
1383
+ );
1384
+ }
1385
+ this.expireInactiveRuns();
1386
+ const calls = this.circuitBreakerRuns.get(identity.key)?.calls ?? 0;
1387
+ return this.createCircuitBreakerDecision(identity.customerId, identity.runId, calls);
1388
+ }
1389
+ /**
1390
+ * Release local state after a workflow finishes. Returns true when state existed.
1391
+ */
1392
+ resetRun(request) {
1393
+ const identity = this.resolveRunIdentity(request);
1394
+ return identity ? this.circuitBreakerRuns.delete(identity.key) : false;
1395
+ }
870
1396
  async promptCompress(request, options = {}) {
871
1397
  if (!request?.callId) {
872
1398
  throw new UsageTapError(
@@ -964,6 +1490,10 @@ var UsageTapClient = class {
964
1490
  usageTapCompressionMessagesEndpoint: this.usageTapCompressionMessagesEndpoint,
965
1491
  aggressiveness: options.aggressiveness ?? this.aggressiveness,
966
1492
  usageTapCompressionAggressiveness: options.usageTapCompressionAggressiveness ?? this.usageTapCompressionAggressiveness,
1493
+ mode: options.mode,
1494
+ latencyBudgetMs: options.latencyBudgetMs,
1495
+ compactEmptyUserMessages: options.compactEmptyUserMessages,
1496
+ compactDuplicateUserTextParts: options.compactDuplicateUserTextParts,
967
1497
  fetchImpl: this.fetchImpl,
968
1498
  signal: options.signal,
969
1499
  failOpen: options.failOpen
@@ -1005,14 +1535,29 @@ var UsageTapClient = class {
1005
1535
  callId: request.callId,
1006
1536
  feature: feature ?? this.defaultFeature,
1007
1537
  tags: tags ?? this.defaultTags,
1538
+ providerUsed: request.providerUsed,
1008
1539
  modelUsed: request.modelUsed,
1540
+ reasoningEffort: request.reasoningEffort,
1541
+ reasoningEffortSource: request.reasoningEffortSource,
1542
+ reasoningMode: request.reasoningMode,
1543
+ reasoningBudgetTokens: request.reasoningBudgetTokens,
1009
1544
  metrics: {
1010
1545
  inputTokens: request.inputTokens,
1011
1546
  responseTokens: request.responseTokens,
1012
1547
  cachedInputTokens: request.cachedInputTokens,
1548
+ cacheWriteInputTokens: request.cacheWriteInputTokens,
1549
+ cacheWrite5mInputTokens: request.cacheWrite5mInputTokens,
1550
+ cacheWrite1hInputTokens: request.cacheWrite1hInputTokens,
1013
1551
  reasoningTokens: request.reasoningTokens,
1014
1552
  searches: request.searches,
1015
1553
  audioSeconds: request.audioSeconds,
1554
+ imageInputCount: request.imageInputCount,
1555
+ imageInputTokens: request.imageInputTokens,
1556
+ imageOutputCount: request.imageOutputCount,
1557
+ imageOutputTokens: request.imageOutputTokens,
1558
+ audioInputTokens: request.audioInputTokens,
1559
+ cachedAudioInputTokens: request.cachedAudioInputTokens,
1560
+ audioOutputTokens: request.audioOutputTokens,
1016
1561
  costUsd: response.data.costUSD
1017
1562
  },
1018
1563
  correlationId: response.correlationId
@@ -1099,10 +1644,10 @@ var UsageTapClient = class {
1099
1644
  "incrementCustomMeter requires meterSlot"
1100
1645
  );
1101
1646
  }
1102
- if (!["CUSTOM1", "CUSTOM2"].includes(request.meterSlot)) {
1647
+ if (!["CUSTOM1", "CUSTOM2", "AGENTIC_API"].includes(request.meterSlot)) {
1103
1648
  throw new UsageTapError(
1104
1649
  "USAGETAP_BAD_REQUEST",
1105
- "meterSlot must be CUSTOM1 or CUSTOM2"
1650
+ "meterSlot must be CUSTOM1, CUSTOM2 or AGENTIC_API"
1106
1651
  );
1107
1652
  }
1108
1653
  if (typeof request.amount !== "number" || !Number.isFinite(request.amount) || request.amount <= 0) {
@@ -1117,6 +1662,15 @@ var UsageTapClient = class {
1117
1662
  meterSlot: request.meterSlot,
1118
1663
  amount: request.amount
1119
1664
  };
1665
+ if (request.customerUserId) {
1666
+ payload.customerUserId = request.customerUserId;
1667
+ }
1668
+ if (request.customerUserName) {
1669
+ payload.customerUserName = request.customerUserName;
1670
+ }
1671
+ if (request.customerUserEmail) {
1672
+ payload.customerUserEmail = request.customerUserEmail;
1673
+ }
1120
1674
  if (request.feature) {
1121
1675
  payload.feature = request.feature;
1122
1676
  }
@@ -1153,6 +1707,11 @@ var UsageTapClient = class {
1153
1707
  const beginPayload = idempotencyKey ? { ...beginRequest, idempotencyKey, idempotency: idempotencyKey } : { ...beginRequest };
1154
1708
  const beginResponse = await this.beginCall(beginPayload, options);
1155
1709
  let usage = {};
1710
+ const pricingMode = beginResponse.data.pricingMode ?? beginRequest.pricingMode ?? (beginRequest.batch === true ? "batch" : beginRequest.batch === false ? "standard" : void 0);
1711
+ if (pricingMode) {
1712
+ usage.pricingMode = pricingMode;
1713
+ usage.batch = pricingMode === "batch";
1714
+ }
1156
1715
  const initialStripeCustomerId = typeof beginResponse.data.stripeCustomerId === "string" ? beginResponse.data.stripeCustomerId : typeof beginRequest.stripeCustomerId === "string" ? beginRequest.stripeCustomerId : void 0;
1157
1716
  if (initialStripeCustomerId) {
1158
1717
  usage = { ...usage, stripeCustomerId: initialStripeCustomerId };
@@ -1161,6 +1720,28 @@ var UsageTapClient = class {
1161
1720
  let handlerResult;
1162
1721
  let handlerError;
1163
1722
  let endCallError;
1723
+ let finalizationDeferred = false;
1724
+ let finalizationPromise;
1725
+ const finalize = () => {
1726
+ if (!finalizationPromise) {
1727
+ finalizationPromise = this.endCall(
1728
+ {
1729
+ callId: beginResponse.data.callId,
1730
+ // Pass context for metric tracking
1731
+ customerId: beginRequest.customerId,
1732
+ feature: beginRequest.feature ?? this.defaultFeature,
1733
+ tags: beginRequest.tags ?? this.defaultTags,
1734
+ ...usage,
1735
+ error: errorPayload
1736
+ },
1737
+ {
1738
+ ...options,
1739
+ correlationId: beginResponse.correlationId
1740
+ }
1741
+ ).then(() => void 0);
1742
+ }
1743
+ return finalizationPromise;
1744
+ };
1164
1745
  const context = {
1165
1746
  begin: beginResponse,
1166
1747
  setUsage: (u) => {
@@ -1168,6 +1749,10 @@ var UsageTapClient = class {
1168
1749
  },
1169
1750
  setError: (err) => {
1170
1751
  errorPayload = err;
1752
+ },
1753
+ deferFinalization: () => {
1754
+ finalizationDeferred = true;
1755
+ return finalize;
1171
1756
  }
1172
1757
  };
1173
1758
  try {
@@ -1181,24 +1766,12 @@ var UsageTapClient = class {
1181
1766
  };
1182
1767
  }
1183
1768
  } finally {
1184
- try {
1185
- await this.endCall(
1186
- {
1187
- callId: beginResponse.data.callId,
1188
- // Pass context for metric tracking
1189
- customerId: beginRequest.customerId,
1190
- feature: beginRequest.feature ?? this.defaultFeature,
1191
- tags: beginRequest.tags ?? this.defaultTags,
1192
- ...usage,
1193
- error: errorPayload
1194
- },
1195
- {
1196
- ...options,
1197
- correlationId: beginResponse.correlationId
1198
- }
1199
- );
1200
- } catch (error) {
1201
- endCallError = error;
1769
+ if (handlerError || !finalizationDeferred) {
1770
+ try {
1771
+ await finalize();
1772
+ } catch (error) {
1773
+ endCallError = error;
1774
+ }
1202
1775
  }
1203
1776
  }
1204
1777
  if (handlerError) {
@@ -1220,17 +1793,86 @@ var UsageTapClient = class {
1220
1793
  toPromptCompressionTelemetry(result) {
1221
1794
  return {
1222
1795
  provider: result.provider,
1223
- originalCharacters: result.originalCharacters,
1224
- compressedCharacters: result.compressedCharacters,
1225
- savedCharacters: result.savedCharacters,
1226
1796
  originalTokens: result.originalTokens,
1227
1797
  compressedTokens: result.compressedTokens,
1228
1798
  savedTokens: result.savedTokens,
1229
1799
  tokenSavingsRatio: result.tokenSavingsRatio,
1230
- savingsRatio: result.savingsRatio,
1231
1800
  techniques: result.techniques
1232
1801
  };
1233
1802
  }
1803
+ reserveRunCall(request, idempotencyKey) {
1804
+ const identity = this.resolveRunIdentity(request);
1805
+ if (!identity || !this.circuitBreaker) return;
1806
+ this.expireInactiveRuns();
1807
+ const now = Date.now();
1808
+ const state = this.circuitBreakerRuns.get(identity.key) ?? {
1809
+ calls: 0,
1810
+ reservationKeys: /* @__PURE__ */ new Set(),
1811
+ lastSeenAtMs: now
1812
+ };
1813
+ const reservationKey = idempotencyKey ?? this.idempotencyGenerator();
1814
+ state.lastSeenAtMs = now;
1815
+ if (state.reservationKeys.has(reservationKey)) {
1816
+ this.circuitBreakerRuns.set(identity.key, state);
1817
+ return;
1818
+ }
1819
+ const decision = this.createCircuitBreakerDecision(
1820
+ identity.customerId,
1821
+ identity.runId,
1822
+ state.calls
1823
+ );
1824
+ if (!decision.allowed) {
1825
+ throw new UsageTapError(
1826
+ "USAGETAP_CIRCUIT_OPEN",
1827
+ `Run ${identity.runId} reached its ${decision.limit}-call circuit-breaker limit`,
1828
+ {
1829
+ details: {
1830
+ reason: decision.reason,
1831
+ customerId: identity.customerId,
1832
+ runId: identity.runId,
1833
+ calls: decision.calls,
1834
+ limit: decision.limit,
1835
+ remaining: decision.remaining
1836
+ }
1837
+ }
1838
+ );
1839
+ }
1840
+ state.calls += 1;
1841
+ state.reservationKeys.add(reservationKey);
1842
+ this.circuitBreakerRuns.set(identity.key, state);
1843
+ }
1844
+ resolveRunIdentity(request) {
1845
+ const customerId = request.customerId?.trim();
1846
+ const runId = request.runId?.trim();
1847
+ if (!customerId || !runId) return void 0;
1848
+ return {
1849
+ key: `${customerId}\0${runId}`,
1850
+ customerId,
1851
+ runId
1852
+ };
1853
+ }
1854
+ createCircuitBreakerDecision(customerId, runId, calls) {
1855
+ const limit = this.circuitBreaker?.maxCallsPerRun ?? 0;
1856
+ const allowed = calls < limit;
1857
+ return {
1858
+ allowed,
1859
+ ...allowed ? {} : { reason: "max_calls_per_run" },
1860
+ customerId,
1861
+ runId,
1862
+ calls,
1863
+ limit,
1864
+ remaining: Math.max(0, limit - calls)
1865
+ };
1866
+ }
1867
+ expireInactiveRuns() {
1868
+ if (!this.circuitBreaker || this.circuitBreakerRuns.size === 0) return;
1869
+ const expiredBefore = Date.now() - this.circuitBreaker.runInactivityMs;
1870
+ for (const [key, state] of this.circuitBreakerRuns) {
1871
+ if (state.lastSeenAtMs < expiredBefore) {
1872
+ this.circuitBreakerRuns.delete(key);
1873
+ }
1874
+ }
1875
+ }
1234
1876
  async request(path, payload, options) {
1235
1877
  const url = new URL(path, this.baseUrl).toString();
1236
1878
  const body = payload !== void 0 ? JSON.stringify(payload) : void 0;
@@ -1418,7 +2060,7 @@ var UsageTapClient = class {
1418
2060
  ...this.defaultHeaders,
1419
2061
  [SDK_HEADER]: `js/${SDK_VERSION}`,
1420
2062
  "content-type": "application/json",
1421
- accept: CANONICAL_MEDIA_TYPE
2063
+ accept: CANONICAL_MEDIA_TYPE2
1422
2064
  };
1423
2065
  if (!HAS_WINDOW) {
1424
2066
  headers["user-agent"] = `${USER_AGENT}/${SDK_VERSION}`;
@@ -1471,7 +2113,8 @@ var UsageTapClient = class {
1471
2113
  }
1472
2114
  toHttpError(status, payload, correlationId) {
1473
2115
  const code = mapStatusToErrorCode(status);
1474
- const retryable = isRetryableStatus(status);
2116
+ const apiCode = payload?.error?.code ?? payload?.result?.code ?? "UNKNOWN";
2117
+ const retryable = isRetryableStatus(status) || isRetryableApiCode(apiCode);
1475
2118
  const message = payload?.error?.message ?? payload?.result?.message ?? `UsageTap responded with HTTP ${status}`;
1476
2119
  return new UsageTapError(code, message, {
1477
2120
  status,
@@ -1504,7 +2147,7 @@ function isRetryableStatus(status) {
1504
2147
  }
1505
2148
  function isRetryableApiCode(code) {
1506
2149
  const normalized = code.toUpperCase();
1507
- return normalized.includes("TRANSIENT") || normalized.includes("RETRY") || normalized.includes("TIMEOUT") || normalized.includes("THROTTLE") || normalized.includes("RATE_LIMIT");
2150
+ return normalized === "PAYG_CONFLICT" || normalized.includes("TRANSIENT") || normalized.includes("RETRY") || normalized.includes("TIMEOUT") || normalized.includes("THROTTLE") || normalized.includes("RATE_LIMIT");
1508
2151
  }
1509
2152
  function mapApiCodeToError(code) {
1510
2153
  const normalized = code.toUpperCase();
@@ -1573,6 +2216,12 @@ function wrapEndCallError(error, correlationId) {
1573
2216
  }
1574
2217
 
1575
2218
  // src/adapters/fetch-wrapper.ts
2219
+ var USAGETAP_CONTEXT_HEADERS = [
2220
+ "x-usagetap-customer-id",
2221
+ "x-usagetap-feature",
2222
+ "x-usagetap-tags",
2223
+ "x-usagetap-run-id"
2224
+ ];
1576
2225
  function isJsonRecord(value) {
1577
2226
  return typeof value === "object" && value !== null && !Array.isArray(value);
1578
2227
  }
@@ -1601,87 +2250,179 @@ function parseStringArray(value) {
1601
2250
  }
1602
2251
  return void 0;
1603
2252
  }
2253
+ function contextOverrideFromHeaders(headers) {
2254
+ const context = {};
2255
+ const customerId = headers.get("x-usagetap-customer-id");
2256
+ const feature = headers.get("x-usagetap-feature");
2257
+ const tags = headers.get("x-usagetap-tags");
2258
+ const runId = headers.get("x-usagetap-run-id");
2259
+ if (customerId) context.customerId = customerId;
2260
+ if (feature) context.feature = feature;
2261
+ if (tags) {
2262
+ const parsed = parseStringArray(tags);
2263
+ if (parsed) context.tags = parsed;
2264
+ }
2265
+ if (runId) context.runId = runId;
2266
+ return context;
2267
+ }
2268
+ function ensureStreamUsageBody(body) {
2269
+ const streamOptions = isJsonRecord(body.stream_options) ? body.stream_options : {};
2270
+ return {
2271
+ ...body,
2272
+ stream_options: {
2273
+ ...streamOptions,
2274
+ include_usage: true
2275
+ }
2276
+ };
2277
+ }
2278
+ async function bodyTextFromInit(body) {
2279
+ if (typeof body === "string") return body;
2280
+ if (body instanceof URLSearchParams) return body.toString();
2281
+ if (body instanceof Blob) return body.text();
2282
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
2283
+ if (ArrayBuffer.isView(body)) {
2284
+ return new TextDecoder().decode(
2285
+ new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
2286
+ );
2287
+ }
2288
+ return void 0;
2289
+ }
1604
2290
  function wrapFetch(usageTap, options) {
1605
2291
  const {
1606
2292
  defaultContext,
1607
2293
  baseFetch = globalThis.fetch,
1608
2294
  autoIdempotency = true,
1609
- isOpenAIEndpoint = defaultIsOpenAIEndpoint
2295
+ isOpenAIEndpoint = defaultIsOpenAIEndpoint,
2296
+ provider = "openai",
2297
+ onMeteringError,
2298
+ strictMetering = false
1610
2299
  } = options;
2300
+ const meteringFailureOptions = { onMeteringError, strictMetering };
1611
2301
  return async function wrappedFetch(input, init) {
1612
2302
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1613
2303
  if (!isOpenAIEndpoint(url)) {
1614
2304
  return baseFetch(input, init);
1615
2305
  }
2306
+ const inputRequest = input instanceof Request ? input : void 0;
2307
+ const effectiveHeaders = new Headers(inputRequest?.headers);
2308
+ new Headers(init?.headers).forEach((value, key) => effectiveHeaders.set(key, value));
2309
+ const vendorHeaders = new Headers(effectiveHeaders);
2310
+ const contextOverride = contextOverrideFromHeaders(effectiveHeaders);
2311
+ for (const header of USAGETAP_CONTEXT_HEADERS) {
2312
+ vendorHeaders.delete(header);
2313
+ }
1616
2314
  let body;
1617
2315
  let isStreaming = false;
1618
- const contextOverride = {};
1619
2316
  try {
1620
- if (init?.body && typeof init.body === "string") {
1621
- const parsedBody = parseJsonRecord(init.body);
1622
- if (!parsedBody) {
1623
- return baseFetch(input, init);
1624
- }
1625
- body = parsedBody;
1626
- isStreaming = parsedBody.stream === true;
1627
- const headers = new Headers(init.headers);
1628
- const customerIdHeader = headers.get("x-usagetap-customer-id");
1629
- const featureHeader = headers.get("x-usagetap-feature");
1630
- const tagsHeader = headers.get("x-usagetap-tags");
1631
- if (customerIdHeader) {
1632
- contextOverride.customerId = customerIdHeader;
1633
- }
1634
- if (featureHeader) {
1635
- contextOverride.feature = featureHeader;
1636
- }
1637
- if (tagsHeader) {
1638
- const tags = parseStringArray(tagsHeader);
1639
- if (tags) {
1640
- contextOverride.tags = tags;
1641
- }
1642
- }
2317
+ if (init?.body) {
2318
+ const bodyText = await bodyTextFromInit(init.body);
2319
+ body = bodyText ? parseJsonRecord(bodyText) : void 0;
2320
+ } else if (inputRequest?.body) {
2321
+ body = parseJsonRecord(await inputRequest.clone().text());
1643
2322
  }
1644
2323
  } catch {
1645
- return baseFetch(input, init);
2324
+ body = void 0;
1646
2325
  }
2326
+ if (!body) {
2327
+ const error = new Error(
2328
+ "Unable to parse the intercepted provider request body as JSON"
2329
+ );
2330
+ await reportMeteringError(meteringFailureOptions, {
2331
+ stage: "request",
2332
+ error
2333
+ });
2334
+ if (strictMetering) throw error;
2335
+ return baseFetch(input, { ...init, headers: vendorHeaders });
2336
+ }
2337
+ isStreaming = body.stream === true;
2338
+ const vendorBody = isStreaming && url.includes("/v1/chat/completions") ? JSON.stringify(ensureStreamUsageBody(body)) : void 0;
1647
2339
  const context = {
1648
2340
  ...defaultContext,
1649
2341
  ...contextOverride,
1650
2342
  idempotency: autoIdempotency ? crypto.randomUUID() : void 0
1651
2343
  };
2344
+ if (requestUsesWebSearch(body)) {
2345
+ context.requested = {
2346
+ ...context.requested ?? {},
2347
+ search: true
2348
+ };
2349
+ }
1652
2350
  let callState;
1653
2351
  try {
1654
2352
  const beginResponse = await usageTap.beginCall(context);
1655
2353
  callState = {
1656
2354
  callId: beginResponse.data.callId,
1657
2355
  correlationId: beginResponse.correlationId,
1658
- usage: {},
1659
- finalized: false
2356
+ usage: {
2357
+ ...executionMetadataFromOpenAIRequest(body, provider),
2358
+ ...beginResponse.data.pricingMode ? {
2359
+ pricingMode: beginResponse.data.pricingMode,
2360
+ batch: beginResponse.data.pricingMode === "batch"
2361
+ } : context.pricingMode ? {
2362
+ pricingMode: context.pricingMode,
2363
+ batch: context.pricingMode === "batch"
2364
+ } : typeof context.batch === "boolean" ? {
2365
+ batch: context.batch,
2366
+ pricingMode: context.batch ? "batch" : "standard"
2367
+ } : {}
2368
+ }
1660
2369
  };
1661
2370
  } catch (error) {
1662
- console.error("[wrapFetch] Failed to begin call:", error);
1663
- return baseFetch(input, init);
2371
+ if (isUsageTapError(error) && error.code === "USAGETAP_CIRCUIT_OPEN") {
2372
+ throw error;
2373
+ }
2374
+ await reportMeteringError(meteringFailureOptions, {
2375
+ stage: "begin",
2376
+ error
2377
+ });
2378
+ if (strictMetering) throw error;
2379
+ return baseFetch(input, {
2380
+ ...init,
2381
+ headers: vendorHeaders,
2382
+ ...vendorBody ? { body: vendorBody } : {}
2383
+ });
2384
+ }
2385
+ if (callState.correlationId) {
2386
+ vendorHeaders.set("x-usage-correlation-id", callState.correlationId);
1664
2387
  }
1665
2388
  const modifiedInit = {
1666
2389
  ...init,
1667
- headers: {
1668
- ...init?.headers,
1669
- "x-usage-correlation-id": callState.correlationId || ""
1670
- }
2390
+ headers: vendorHeaders,
2391
+ ...vendorBody ? { body: vendorBody } : {}
1671
2392
  };
1672
2393
  try {
1673
2394
  const response = await baseFetch(input, modifiedInit);
2395
+ if (!response.ok) {
2396
+ await finalizeHttpError(
2397
+ response,
2398
+ callState,
2399
+ usageTap,
2400
+ meteringFailureOptions
2401
+ );
2402
+ return response;
2403
+ }
1674
2404
  if (isStreaming) {
1675
- return wrapStreamingResponse(response, callState, usageTap);
2405
+ return await wrapStreamingResponse(
2406
+ response,
2407
+ callState,
2408
+ usageTap,
2409
+ meteringFailureOptions
2410
+ );
1676
2411
  } else {
1677
- return await wrapNonStreamingResponse(response, callState, usageTap, body);
2412
+ return await wrapNonStreamingResponse(
2413
+ response,
2414
+ callState,
2415
+ usageTap,
2416
+ body,
2417
+ meteringFailureOptions
2418
+ );
1678
2419
  }
1679
2420
  } catch (error) {
1680
2421
  const message = error instanceof Error ? error.message : String(error);
1681
- await finalizeCall(callState, usageTap, {
2422
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
1682
2423
  code: "VENDOR_ERROR",
1683
2424
  message
1684
- });
2425
+ }, { responseErrorMessage: message });
1685
2426
  throw error;
1686
2427
  }
1687
2428
  };
@@ -1689,137 +2430,277 @@ function wrapFetch(usageTap, options) {
1689
2430
  function defaultIsOpenAIEndpoint(url) {
1690
2431
  return url.includes("/v1/chat/completions") || url.includes("/v1/responses") || url.includes("/v1/embeddings");
1691
2432
  }
1692
- async function wrapNonStreamingResponse(response, callState, usageTap, requestBody) {
2433
+ function requestUsesWebSearch(body) {
2434
+ return Array.isArray(body?.tools) && body.tools.some((tool) => {
2435
+ if (!isJsonRecord(tool)) return false;
2436
+ return tool.type === "web_search" || tool.type === "web_search_preview";
2437
+ });
2438
+ }
2439
+ function usageFromOpenAIResponse(parsed, provider) {
2440
+ const usage = {};
2441
+ const usageBlock = parsed.usage;
2442
+ if (isJsonRecord(usageBlock)) {
2443
+ const usageRecord = usageBlock;
2444
+ const promptDetails = isJsonRecord(
2445
+ usageRecord.prompt_tokens_details ?? usageRecord.input_tokens_details
2446
+ ) ? usageRecord.prompt_tokens_details ?? usageRecord.input_tokens_details : void 0;
2447
+ const completionDetails = isJsonRecord(
2448
+ usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details
2449
+ ) ? usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details : void 0;
2450
+ const cachedTokenDetails = isJsonRecord(promptDetails?.cached_tokens_details) ? promptDetails.cached_tokens_details : void 0;
2451
+ const cacheCreation = isJsonRecord(usageRecord.cache_creation) ? usageRecord.cache_creation : void 0;
2452
+ const promptTokens = readNumber(
2453
+ usageRecord.prompt_tokens ?? usageRecord.input_tokens
2454
+ );
2455
+ if (promptTokens !== void 0) usage.inputTokens = promptTokens;
2456
+ const completionTokens = readNumber(
2457
+ usageRecord.completion_tokens ?? usageRecord.output_tokens
2458
+ );
2459
+ if (completionTokens !== void 0) usage.responseTokens = completionTokens;
2460
+ const cachedTokens = readNumber(
2461
+ usageRecord.prompt_cache_hit_tokens ?? usageRecord.cache_read_input_tokens ?? usageRecord.cached_tokens ?? promptDetails?.cached_tokens
2462
+ );
2463
+ if (cachedTokens !== void 0) usage.cachedInputTokens = cachedTokens;
2464
+ const cacheWriteTokens = readNumber(
2465
+ usageRecord.cache_creation_input_tokens ?? usageRecord.cache_write_input_tokens ?? usageRecord.cache_write_tokens ?? promptDetails?.cache_write_tokens ?? promptDetails?.cache_creation_tokens
2466
+ );
2467
+ if (cacheWriteTokens !== void 0) {
2468
+ usage.cacheWriteInputTokens = cacheWriteTokens;
2469
+ }
2470
+ const cacheWrite5m = readNumber(
2471
+ usageRecord.cache_write_5m_input_tokens ?? cacheCreation?.ephemeral_5m_input_tokens
2472
+ );
2473
+ if (cacheWrite5m !== void 0) usage.cacheWrite5mInputTokens = cacheWrite5m;
2474
+ const cacheWrite1h = readNumber(
2475
+ usageRecord.cache_write_1h_input_tokens ?? cacheCreation?.ephemeral_1h_input_tokens
2476
+ );
2477
+ if (cacheWrite1h !== void 0) usage.cacheWrite1hInputTokens = cacheWrite1h;
2478
+ if (provider?.toLowerCase() === "anthropic" && promptTokens !== void 0) {
2479
+ usage.inputTokens = promptTokens + (cachedTokens ?? 0) + (cacheWriteTokens ?? 0);
2480
+ }
2481
+ const audioInputTokens = readNumber(promptDetails?.audio_tokens);
2482
+ if (audioInputTokens !== void 0) usage.audioInputTokens = audioInputTokens;
2483
+ const cachedAudioInputTokens = readNumber(
2484
+ promptDetails?.cached_audio_tokens ?? cachedTokenDetails?.audio_tokens
2485
+ );
2486
+ if (cachedAudioInputTokens !== void 0) {
2487
+ usage.cachedAudioInputTokens = cachedAudioInputTokens;
2488
+ }
2489
+ const imageInputTokens = readNumber(promptDetails?.image_tokens);
2490
+ if (imageInputTokens !== void 0) usage.imageInputTokens = imageInputTokens;
2491
+ const imageOutputTokens = readNumber(completionDetails?.image_tokens);
2492
+ if (imageOutputTokens !== void 0) usage.imageOutputTokens = imageOutputTokens;
2493
+ const audioOutputTokens = readNumber(completionDetails?.audio_tokens);
2494
+ if (audioOutputTokens !== void 0) usage.audioOutputTokens = audioOutputTokens;
2495
+ const reasoningTokens = readNumber(
2496
+ usageRecord.reasoning_tokens ?? completionDetails?.reasoning_tokens
2497
+ );
2498
+ if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
2499
+ const serverToolUse = isJsonRecord(usageRecord.server_tool_use) ? usageRecord.server_tool_use : void 0;
2500
+ const outputSearches = Array.isArray(parsed.output) ? parsed.output.filter(
2501
+ (item) => isJsonRecord(item) && item.type === "web_search_call"
2502
+ ).length : 0;
2503
+ const searches = readNumber(
2504
+ usageRecord.searches ?? usageRecord.web_search_queries ?? serverToolUse?.web_search_requests ?? (outputSearches > 0 ? outputSearches : void 0)
2505
+ );
2506
+ if (searches !== void 0 && searches > 0) usage.searches = searches;
2507
+ }
2508
+ const modelFromResponse = readString(parsed.model);
2509
+ if (modelFromResponse) usage.modelUsed = modelFromResponse;
2510
+ const reasoning = isJsonRecord(parsed.reasoning) ? parsed.reasoning : void 0;
2511
+ const responseEffort = normalizeReasoningEffort(reasoning?.effort);
2512
+ if (responseEffort) {
2513
+ usage.reasoningEffort = responseEffort;
2514
+ usage.reasoningEffortSource = "provider_response";
2515
+ }
2516
+ const responseMode = readString(reasoning?.type ?? reasoning?.mode);
2517
+ if (responseMode) usage.reasoningMode = responseMode;
2518
+ return usage;
2519
+ }
2520
+ async function wrapNonStreamingResponse(response, callState, usageTap, requestBody, meteringFailureOptions) {
1693
2521
  const clonedResponse = response.clone();
1694
2522
  try {
1695
2523
  const parsed = await clonedResponse.json();
1696
- const usage = {};
1697
- if (isJsonRecord(parsed)) {
1698
- const usageBlock = parsed.usage;
1699
- if (isJsonRecord(usageBlock)) {
1700
- const usageRecord = usageBlock;
1701
- const promptDetails = isJsonRecord(usageRecord.prompt_tokens_details) ? usageRecord.prompt_tokens_details : void 0;
1702
- const promptTokens = readNumber(usageRecord.prompt_tokens ?? usageRecord.input_tokens);
1703
- if (promptTokens !== void 0) {
1704
- usage.inputTokens = promptTokens;
1705
- }
1706
- const completionTokens = readNumber(usageRecord.completion_tokens ?? usageRecord.output_tokens);
1707
- if (completionTokens !== void 0) {
1708
- usage.responseTokens = completionTokens;
1709
- }
1710
- const cachedTokens = readNumber(
1711
- usageRecord.prompt_cache_hit_tokens ?? usageRecord.cache_read_input_tokens ?? usageRecord.cached_tokens ?? promptDetails?.cached_tokens
1712
- );
1713
- if (cachedTokens !== void 0) {
1714
- usage.cachedInputTokens = cachedTokens;
1715
- }
1716
- const reasoningTokens = readNumber(usageRecord.reasoning_tokens);
1717
- if (reasoningTokens !== void 0) {
1718
- usage.reasoningTokens = reasoningTokens;
1719
- }
1720
- }
1721
- const modelFromResponse = readString(parsed.model);
1722
- if (modelFromResponse) {
1723
- usage.modelUsed = modelFromResponse;
1724
- }
1725
- }
2524
+ const usage = isJsonRecord(parsed) ? usageFromOpenAIResponse(parsed, readString(callState.usage.providerUsed)) : {};
1726
2525
  if (!usage.modelUsed && requestBody) {
1727
2526
  const requestModel = readString(requestBody.model);
1728
2527
  if (requestModel) {
1729
2528
  usage.modelUsed = requestModel;
1730
2529
  }
1731
2530
  }
1732
- await finalizeCall(callState, usageTap, void 0, usage);
2531
+ await finalizeCall(callState, usageTap, meteringFailureOptions, void 0, {
2532
+ ...usage,
2533
+ responseStatusCode: response.status
2534
+ });
1733
2535
  return response;
1734
2536
  } catch (error) {
1735
2537
  const message = error instanceof Error ? error.message : String(error);
1736
- await finalizeCall(callState, usageTap, {
2538
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
1737
2539
  code: "RESPONSE_PARSE_ERROR",
1738
2540
  message
2541
+ }, {
2542
+ responseStatusCode: response.status,
2543
+ responseErrorMessage: message
1739
2544
  });
1740
2545
  return response;
1741
2546
  }
1742
2547
  }
1743
- function wrapStreamingResponse(response, callState, usageTap) {
2548
+ async function finalizeHttpError(response, callState, usageTap, meteringFailureOptions) {
2549
+ let message = `${response.status} ${response.statusText || "Provider request failed"}`.trim();
2550
+ try {
2551
+ const text = await response.clone().text();
2552
+ const parsed = parseJsonRecord(text);
2553
+ const errorRecord = parsed && isJsonRecord(parsed.error) ? parsed.error : void 0;
2554
+ message = readString(errorRecord?.message) ?? readString(parsed?.message) ?? (text.trim() || message);
2555
+ } catch {
2556
+ }
2557
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
2558
+ code: "VENDOR_HTTP_ERROR",
2559
+ message
2560
+ }, {
2561
+ responseStatusCode: response.status,
2562
+ responseErrorMessage: message
2563
+ });
2564
+ }
2565
+ async function wrapStreamingResponse(response, callState, usageTap, meteringFailureOptions) {
1744
2566
  if (!response.body) {
1745
- void finalizeCall(callState, usageTap, {
2567
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
1746
2568
  code: "NO_RESPONSE_BODY",
1747
2569
  message: "Streaming response has no body"
2570
+ }, {
2571
+ responseStatusCode: response.status,
2572
+ responseErrorMessage: "Streaming response has no body"
1748
2573
  });
1749
2574
  return response;
1750
2575
  }
1751
- const originalBody = response.body;
2576
+ const reader = response.body.getReader();
1752
2577
  const accumulatedUsage = {};
1753
2578
  const textDecoder = new TextDecoder();
1754
- const transformStream = new TransformStream({
1755
- transform(chunk, controller) {
1756
- controller.enqueue(chunk);
2579
+ let pendingSseText = "";
2580
+ const applyUsageEvent = (eventText) => {
2581
+ const data = eventText.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
2582
+ if (!data || data === "[DONE]") return;
2583
+ const parsedRecord = parseJsonRecord(data);
2584
+ if (!parsedRecord) return;
2585
+ const responseRecord = isJsonRecord(parsedRecord.response) ? parsedRecord.response : parsedRecord;
2586
+ Object.assign(
2587
+ accumulatedUsage,
2588
+ usageFromOpenAIResponse(
2589
+ responseRecord,
2590
+ readString(callState.usage.providerUsed)
2591
+ )
2592
+ );
2593
+ };
2594
+ const applyCompleteSseEvents = (final = false) => {
2595
+ const events = pendingSseText.split(/\r?\n\r?\n/);
2596
+ const tail = events.pop() ?? "";
2597
+ pendingSseText = final ? "" : tail;
2598
+ for (const eventText of events) applyUsageEvent(eventText);
2599
+ if (final && tail) applyUsageEvent(tail);
2600
+ };
2601
+ const finalizeStream = async (error) => {
2602
+ pendingSseText += textDecoder.decode();
2603
+ applyCompleteSseEvents(true);
2604
+ await finalizeCall(
2605
+ callState,
2606
+ usageTap,
2607
+ meteringFailureOptions,
2608
+ error,
2609
+ {
2610
+ ...accumulatedUsage,
2611
+ responseStatusCode: response.status,
2612
+ ...error ? { responseErrorMessage: error.message } : {}
2613
+ }
2614
+ );
2615
+ };
2616
+ const wrappedBody = new ReadableStream({
2617
+ async pull(controller) {
1757
2618
  try {
1758
- const text = textDecoder.decode(chunk, { stream: true });
1759
- const lines = text.split("\n");
1760
- for (const line of lines) {
1761
- if (line.startsWith("data: ")) {
1762
- const data = line.slice(6);
1763
- if (data === "[DONE]") continue;
1764
- const parsedRecord = parseJsonRecord(data);
1765
- if (!parsedRecord) {
1766
- continue;
1767
- }
1768
- const usageBlock = parsedRecord.usage;
1769
- if (isJsonRecord(usageBlock)) {
1770
- const usageRecord = usageBlock;
1771
- const promptDetails = isJsonRecord(usageRecord.prompt_tokens_details) ? usageRecord.prompt_tokens_details : void 0;
1772
- const promptTokens = readNumber(usageRecord.prompt_tokens ?? usageRecord.input_tokens);
1773
- if (promptTokens !== void 0) {
1774
- accumulatedUsage.inputTokens = promptTokens;
1775
- }
1776
- const completionTokens = readNumber(usageRecord.completion_tokens ?? usageRecord.output_tokens);
1777
- if (completionTokens !== void 0) {
1778
- accumulatedUsage.responseTokens = completionTokens;
1779
- }
1780
- const cachedTokens = readNumber(
1781
- usageRecord.prompt_cache_hit_tokens ?? usageRecord.cache_read_input_tokens ?? usageRecord.cached_tokens ?? promptDetails?.cached_tokens
1782
- );
1783
- if (cachedTokens !== void 0) {
1784
- accumulatedUsage.cachedInputTokens = cachedTokens;
1785
- }
1786
- const reasoningTokens = readNumber(usageRecord.reasoning_tokens);
1787
- if (reasoningTokens !== void 0) {
1788
- accumulatedUsage.reasoningTokens = reasoningTokens;
1789
- }
1790
- }
1791
- const model = readString(parsedRecord.model);
1792
- if (model) {
1793
- accumulatedUsage.modelUsed = model;
1794
- }
1795
- }
2619
+ const result = await reader.read();
2620
+ if (result.done) {
2621
+ await finalizeStream();
2622
+ controller.close();
2623
+ return;
1796
2624
  }
1797
- } catch {
2625
+ const chunk = result.value;
2626
+ pendingSseText += textDecoder.decode(chunk, { stream: true });
2627
+ applyCompleteSseEvents();
2628
+ controller.enqueue(chunk);
2629
+ } catch (error) {
2630
+ const message = error instanceof Error ? error.message : String(error);
2631
+ await finalizeStream({ code: "VENDOR_STREAM_ERROR", message }).catch(() => void 0);
2632
+ controller.error(error);
1798
2633
  }
1799
2634
  },
1800
- async flush() {
1801
- await finalizeCall(callState, usageTap, void 0, accumulatedUsage);
2635
+ async cancel(reason) {
2636
+ const message = reason instanceof Error ? reason.message : typeof reason === "string" && reason ? reason : "Provider stream consumption was cancelled before completion";
2637
+ try {
2638
+ await reader.cancel(reason);
2639
+ } finally {
2640
+ await finalizeStream({ code: "STREAM_ABORTED", message });
2641
+ }
1802
2642
  }
1803
2643
  });
1804
- const wrappedBody = originalBody.pipeThrough(transformStream);
1805
2644
  return new Response(wrappedBody, {
1806
2645
  status: response.status,
1807
2646
  statusText: response.statusText,
1808
2647
  headers: response.headers
1809
2648
  });
1810
2649
  }
1811
- async function finalizeCall(callState, usageTap, error, usage) {
1812
- if (callState.finalized) return;
1813
- callState.finalized = true;
2650
+ function normalizeReasoningEffort(value) {
2651
+ return value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max" ? value : void 0;
2652
+ }
2653
+ function executionMetadataFromOpenAIRequest(body, provider) {
2654
+ const reasoning = isJsonRecord(body?.reasoning) ? body.reasoning : void 0;
2655
+ const effort = normalizeReasoningEffort(
2656
+ body?.reasoning_effort ?? reasoning?.effort ?? body?.thinking_level
2657
+ );
2658
+ const mode = readString(reasoning?.type ?? reasoning?.mode);
2659
+ const budget = readNumber(
2660
+ reasoning?.budget_tokens ?? body?.thinking_budget ?? body?.thinking_budget_tokens
2661
+ );
2662
+ return {
2663
+ providerUsed: provider,
2664
+ ...typeof body?.model === "string" ? { modelUsed: body.model } : {},
2665
+ ...effort ? { reasoningEffort: effort, reasoningEffortSource: "provider_request" } : {},
2666
+ ...mode ? { reasoningMode: mode } : {},
2667
+ ...budget !== void 0 ? { reasoningBudgetTokens: budget } : {}
2668
+ };
2669
+ }
2670
+ async function finalizeCall(callState, usageTap, meteringFailureOptions, error, usage) {
2671
+ if (!callState.finalizationPromise) {
2672
+ callState.finalizationPromise = (async () => {
2673
+ try {
2674
+ await usageTap.endCall({
2675
+ callId: callState.callId,
2676
+ ...callState.usage,
2677
+ ...usage,
2678
+ error
2679
+ });
2680
+ } catch (endError) {
2681
+ await reportMeteringError(meteringFailureOptions, {
2682
+ stage: "end",
2683
+ callId: callState.callId,
2684
+ error: endError
2685
+ });
2686
+ if (meteringFailureOptions.strictMetering) {
2687
+ throw endError;
2688
+ }
2689
+ }
2690
+ })();
2691
+ }
2692
+ await callState.finalizationPromise;
2693
+ }
2694
+ async function reportMeteringError(options, event) {
2695
+ console.error("[wrapFetch] Metering failure", {
2696
+ stage: event.stage,
2697
+ callId: event.callId,
2698
+ message: event.error instanceof Error ? event.error.message : String(event.error)
2699
+ });
1814
2700
  try {
1815
- await usageTap.endCall({
1816
- callId: callState.callId,
1817
- ...callState.usage,
1818
- ...usage,
1819
- error
1820
- });
1821
- } catch (err) {
1822
- console.error("[wrapFetch] Failed to finalize call:", err);
2701
+ await options.onMeteringError?.(event);
2702
+ } catch (callbackError) {
2703
+ console.error("[wrapFetch] onMeteringError callback failed", callbackError);
1823
2704
  }
1824
2705
  }
1825
2706