@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.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: {
|
|
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
|
|
1142
|
+
var CANONICAL_MEDIA_TYPE2 = "application/vnd.usagetap.v1+json";
|
|
771
1143
|
var DEFAULT_BASE_URL = "https://api.usagetap.com";
|
|
772
|
-
var
|
|
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
|
|
822
|
-
this.baseUrl = new URL(
|
|
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
|
-
...
|
|
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
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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:
|
|
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
|
|
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
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
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(
|
|
2403
|
+
return await wrapStreamingResponse(
|
|
2404
|
+
response,
|
|
2405
|
+
callState,
|
|
2406
|
+
usageTap,
|
|
2407
|
+
meteringFailureOptions
|
|
2408
|
+
);
|
|
1674
2409
|
} else {
|
|
1675
|
-
return await wrapNonStreamingResponse(
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
2574
|
+
const reader = response.body.getReader();
|
|
1750
2575
|
const accumulatedUsage = {};
|
|
1751
2576
|
const textDecoder = new TextDecoder();
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
|
|
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
|
|
1799
|
-
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
|