@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.
- package/README.md +372 -39
- package/dist/adapters/anthropic.cjs +995 -69
- package/dist/adapters/anthropic.cjs.map +1 -1
- package/dist/adapters/anthropic.d.cts +45 -3
- package/dist/adapters/anthropic.d.ts +45 -3
- package/dist/adapters/anthropic.mjs +995 -70
- package/dist/adapters/anthropic.mjs.map +1 -1
- package/dist/adapters/openai.cjs +1208 -106
- package/dist/adapters/openai.cjs.map +1 -1
- package/dist/adapters/openai.d.cts +46 -3
- package/dist/adapters/openai.d.ts +46 -3
- package/dist/adapters/openai.mjs +1208 -107
- package/dist/adapters/openai.mjs.map +1 -1
- package/dist/adapters/openrouter.cjs +3912 -53
- package/dist/adapters/openrouter.cjs.map +1 -1
- package/dist/adapters/openrouter.d.cts +6 -3
- package/dist/adapters/openrouter.d.ts +6 -3
- package/dist/adapters/openrouter.mjs +3910 -54
- package/dist/adapters/openrouter.mjs.map +1 -1
- package/dist/anthropic/index.cjs +995 -69
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.d.cts +2 -2
- package/dist/anthropic/index.d.ts +2 -2
- package/dist/anthropic/index.mjs +995 -70
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/client-C0UiaqVB.d.cts +1305 -0
- package/dist/client-C0UiaqVB.d.ts +1305 -0
- package/dist/express/index.cjs +399 -64
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +2 -2
- package/dist/express/index.d.ts +2 -2
- package/dist/express/index.mjs +399 -64
- package/dist/express/index.mjs.map +1 -1
- package/dist/index.cjs +1044 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -5
- package/dist/index.d.ts +16 -5
- package/dist/index.mjs +1044 -163
- package/dist/index.mjs.map +1 -1
- package/dist/openai/index.cjs +1209 -107
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.d.cts +2 -2
- package/dist/openai/index.d.ts +2 -2
- package/dist/openai/index.mjs +1209 -108
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openrouter/index.cjs +1226 -109
- package/dist/openrouter/index.cjs.map +1 -1
- package/dist/openrouter/index.d.cts +3 -3
- package/dist/openrouter/index.d.ts +3 -3
- package/dist/openrouter/index.mjs +1224 -108
- package/dist/openrouter/index.mjs.map +1 -1
- package/dist/react/index.cjs +19 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +17 -4
- package/dist/react/index.d.ts +17 -4
- package/dist/react/index.mjs +19 -1
- package/dist/react/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/client-BD8O2J8Z.d.cts +0 -668
- 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: {
|
|
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
|
|
1144
|
+
var CANONICAL_MEDIA_TYPE2 = "application/vnd.usagetap.v1+json";
|
|
773
1145
|
var DEFAULT_BASE_URL = "https://api.usagetap.com";
|
|
774
|
-
var
|
|
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
|
|
824
|
-
this.baseUrl = new URL(
|
|
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
|
-
...
|
|
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
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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:
|
|
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
|
|
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
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
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(
|
|
2405
|
+
return await wrapStreamingResponse(
|
|
2406
|
+
response,
|
|
2407
|
+
callState,
|
|
2408
|
+
usageTap,
|
|
2409
|
+
meteringFailureOptions
|
|
2410
|
+
);
|
|
1676
2411
|
} else {
|
|
1677
|
-
return await wrapNonStreamingResponse(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
2576
|
+
const reader = response.body.getReader();
|
|
1752
2577
|
const accumulatedUsage = {};
|
|
1753
2578
|
const textDecoder = new TextDecoder();
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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
|
-
|
|
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
|
|
1801
|
-
|
|
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
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
|