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