UNPKG

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
'use strict'; 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