ai-sdk-token-usage
Version:
A lightweight Typescript library to track and visualize token usage across multiple AI model providers.
305 lines (297 loc) • 9.55 kB
JavaScript
;
var useSWR = require('swr');
var react = require('react');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var useSWR__default = /*#__PURE__*/_interopDefault(useSWR);
// src/core/format.ts
function formatTokenAmount(tokens) {
return new Intl.NumberFormat("en-US", {
notation: "compact"
}).format(tokens);
}
function formatPrice(price, currency = "USD") {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency
}).format(price);
}
// src/core/errors.ts
var BaseError = class extends Error {
status;
info;
constructor(status, info, message) {
super(message ?? "An error occurred");
this.name = "BaseError";
this.status = status;
this.info = info;
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
info: this.info
};
}
};
var FetchError = class extends BaseError {
constructor(status, info, message) {
super(
status,
info,
message ?? "Network request failed or returned an unexpected status. Check `status` and `info` for details."
);
this.name = "FetchError";
}
};
var ModelNotFoundError = class extends BaseError {
constructor(info) {
super(
404,
info,
"Model not found in catalog. Verify the model ID/provider, or inspect the catalog at https://models.dev."
);
this.name = "ModelNotFoundError";
}
};
var MissingMetadataError = class extends BaseError {
constructor(info) {
super(
422,
info,
"Message metadata is missing or invalid. Expected { totalUsage: LanguageModelUsage, canonicalSlug: string }. Extra fields are allowed."
);
this.name = "MissingMetadataError";
}
};
var CostComputationError = class extends BaseError {
constructor(info) {
super(
404,
info,
"Cost computation failed: pricing is missing for one or more models. Visit https://models.dev to see the catalog"
);
this.name = "CostComputationError";
}
};
var UnknownError = class extends BaseError {
constructor() {
super(500, void 0, "An unknown error occured");
this.name = "UnknownError";
}
};
// src/core/hooks/helpers.ts
function toTokenUsageError(err) {
return err instanceof BaseError ? err.toJSON() : new UnknownError().toJSON();
}
function resultError(error) {
return {
data: void 0,
isLoading: false,
error
};
}
function resultLoading() {
return {
data: void 0,
isLoading: true,
error: null
};
}
function resultSuccess(data) {
return {
data,
isLoading: false,
error: null
};
}
var DEFAULT_POLICY = { reasoningBakedIn: false };
var PROVIDER_POLICY = {
openai: { reasoningBakedIn: true },
google: { reasoningBakedIn: false },
anthropic: { reasoningBakedIn: false }
};
function getPolicy(providerId) {
return PROVIDER_POLICY[providerId] ?? DEFAULT_POLICY;
}
function normalizeTokenUsage(message, stripEmptyReasoning = false) {
const { totalUsage, canonicalSlug } = message.metadata;
const { providerId } = parseCanonicalSlug(canonicalSlug);
const policy = getPolicy(providerId);
const input = totalUsage.inputTokens ?? 0;
const cachedInput = totalUsage.cachedInputTokens ?? 0;
const output = totalUsage.outputTokens ?? 0;
const reasoning = totalUsage.reasoningTokens ?? 0;
const reasoningPart = message.parts.find((p) => p.type === "reasoning");
const hasEmptyReasoningPart = reasoningPart && reasoningPart.text.trim() === "";
const shouldZeroReasoning = hasEmptyReasoningPart && stripEmptyReasoning;
return {
input,
output: policy.reasoningBakedIn ? Math.max(0, output - reasoning) : output,
reasoning: shouldZeroReasoning ? 0 : reasoning,
cachedInput
};
}
function parseCanonicalSlug(slug) {
const [providerId = "", modelId = ""] = slug.split("/", 2);
return { providerId, modelId };
}
function hasInvalidTokenUsageMetadata(message) {
if (!message) return false;
const meta = message.metadata;
if (meta == null || typeof meta !== "object" || Array.isArray(meta)) return true;
const m = meta;
const hasCanonicalSlug = typeof m.canonicalSlug === "string";
const hasTotalUsage = typeof m.totalUsage === "object" && m.totalUsage !== null && !Array.isArray(m.totalUsage);
return !(hasCanonicalSlug && hasTotalUsage);
}
async function fetchModels(url) {
const res = await fetch(url);
if (!res.ok) {
throw new FetchError(res.status, await res.json());
}
return res.json();
}
function useModels() {
const { data, isLoading, error } = useSWR__default.default("/__models.dev", fetchModels);
return {
data,
isLoading,
error
};
}
// src/core/hooks/use-model-details.ts
function useModelDetails({ canonicalSlug }) {
const { data: models, isLoading, error } = useModels();
if (isLoading) return resultLoading();
if (error) return resultError(error.toJSON());
const { providerId, modelId } = parseCanonicalSlug(canonicalSlug);
const model = models?.[providerId]?.models[modelId];
if (!model) {
return resultError(new ModelNotFoundError({ providerId, modelId }).toJSON());
}
if (!model.cost) {
return resultError(new CostComputationError({ providerId, modelId }));
}
const cost = model.cost;
const limit = model.limit;
const modelDetails = {
canonicalSlug,
pricing: {
input: cost.input,
output: cost.output,
reasoning: cost.reasoning ?? cost.output,
cachedInput: cost.cache_read ?? cost.input
},
limit
};
return resultSuccess(modelDetails);
}
function findLast(arr, pred) {
for (let i = arr.length - 1; i >= 0; i--) {
const v = arr[i];
if (v === void 0) continue;
if (pred(v)) return v;
}
return void 0;
}
function computeContext(message, model) {
if (message && hasInvalidTokenUsageMetadata(message)) {
throw new MissingMetadataError({ message, metadata: message.metadata });
}
const breakdown = message ? normalizeTokenUsage(message, true) : { input: 0, output: 0, reasoning: 0, cachedInput: 0 };
const used = Object.values(breakdown).reduce((sum, v) => sum + v, 0);
const limit = model.limit.context;
const remaining = Math.max(0, limit - used);
const fractionUsed = limit > 0 ? used / limit : 0;
const percentageUsed = fractionUsed * 100;
const isExceeded = used > limit;
return {
breakdown,
used,
limit,
remaining,
fractionUsed,
percentageUsed,
isExceeded
};
}
function useTokenContext({
messages,
canonicalSlug
}) {
const { data: models, isLoading, error } = useModels();
const mostRecentAssistantMessage = react.useMemo(
() => findLast(messages, (m) => m.role === "assistant" && m.metadata !== void 0),
[messages]
);
if (isLoading) return resultLoading();
if (error) return resultError(error.toJSON());
const { providerId, modelId } = parseCanonicalSlug(canonicalSlug);
const model = models?.[providerId]?.models[modelId];
if (!model) {
return resultError(new ModelNotFoundError({ modelId, providerId }).toJSON());
}
try {
return resultSuccess(computeContext(mostRecentAssistantMessage, model));
} catch (err) {
return resultError(toTokenUsageError(err));
}
}
function computeCost(messages, resolveModel) {
const breakdown = {
input: { amount: 0, cost: 0 },
output: { amount: 0, cost: 0 },
reasoning: { amount: 0, cost: 0 },
cachedInput: { amount: 0, cost: 0 }
};
messages.forEach((m) => {
if (hasInvalidTokenUsageMetadata(m)) {
throw new MissingMetadataError({ message: m, metadata: m.metadata });
}
const { canonicalSlug } = m.metadata;
const { providerId, modelId } = parseCanonicalSlug(canonicalSlug);
const model = resolveModel({ providerId, modelId });
if (!model) {
throw new ModelNotFoundError({ providerId, modelId });
}
if (!model.cost) {
throw new CostComputationError({ providerId, modelId });
}
const tokens = normalizeTokenUsage(m);
const cost = model.cost;
breakdown.input.amount += tokens.input;
breakdown.input.cost += tokens.input / 1e6 * cost.input;
breakdown.output.amount += tokens.output;
breakdown.output.cost += tokens.output / 1e6 * cost.output;
breakdown.reasoning.amount += tokens.reasoning;
breakdown.reasoning.cost += tokens.reasoning / 1e6 * (cost.reasoning ?? cost.output);
breakdown.cachedInput.amount += tokens.cachedInput;
breakdown.cachedInput.cost += tokens.cachedInput / 1e6 * (cost.cache_read ?? cost.input);
});
const total = Object.values(breakdown).reduce((sum, v) => sum + v.cost, 0);
return { breakdown, total, currency: "USD" };
}
function useTokenCost(params) {
const { data: models, isLoading, error } = useModels();
const messages = "messages" in params ? params.messages : [params.message];
const assistantMessages = react.useMemo(
() => messages.filter((m) => m.role === "assistant" && m.metadata !== void 0),
[messages]
);
if (isLoading) return resultLoading();
if (error) return resultError(error.toJSON());
const resolveModel = ({ providerId, modelId }) => models?.[providerId]?.models?.[modelId];
try {
return resultSuccess(computeCost(assistantMessages, resolveModel));
} catch (err) {
return resultError(toTokenUsageError(err));
}
}
exports.formatPrice = formatPrice;
exports.formatTokenAmount = formatTokenAmount;
exports.useModelDetails = useModelDetails;
exports.useTokenContext = useTokenContext;
exports.useTokenCost = useTokenCost;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map