ccusage
Version:
Usage analysis tool for Claude Code
408 lines (407 loc) • 15.4 kB
JavaScript
import { modelPricingSchema } from "./_types-Cr2YEzKm.js";
import { logger } from "./logger-Cke8hliP.js";
import { createRequire } from "node:module";
import F, { homedir } from "node:os";
import path from "node:path";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
var __require = /* @__PURE__ */ createRequire(import.meta.url);
const homeDirectory = F.homedir();
const { env } = process;
const xdgData = env.XDG_DATA_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "share") : void 0);
const xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path.join(homeDirectory, ".config") : void 0);
const xdgState = env.XDG_STATE_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "state") : void 0);
const xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, ".cache") : void 0);
const xdgRuntime = env.XDG_RUNTIME_DIR || void 0;
const xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
if (xdgData) xdgDataDirectories.unshift(xdgData);
const xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
if (xdgConfig) xdgConfigDirectories.unshift(xdgConfig);
/**
* URL for LiteLLM's model pricing and context window data
*/
const LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
/**
* Default number of recent days to include when filtering blocks
* Used in both session blocks and commands for consistent behavior
*/
const DEFAULT_RECENT_DAYS = 3;
/**
* Threshold percentage for showing usage warnings in blocks command (80%)
* When usage exceeds this percentage of limits, warnings are displayed
*/
const BLOCKS_WARNING_THRESHOLD = .8;
/**
* Terminal width threshold for switching to compact display mode in blocks command
* Below this width, tables use more compact formatting
*/
const BLOCKS_COMPACT_WIDTH_THRESHOLD = 120;
/**
* Default terminal width when stdout.columns is not available in blocks command
* Used as fallback for responsive table formatting
*/
const BLOCKS_DEFAULT_TERMINAL_WIDTH = 120;
/**
* Threshold percentage for considering costs as matching (0.1% tolerance)
* Used in debug cost validation to allow for minor calculation differences
*/
const DEBUG_MATCH_THRESHOLD_PERCENT = .1;
/**
* User's home directory path
* Centralized access to OS home directory for consistent path building
*/
const USER_HOME_DIR = homedir();
/**
* XDG config directory path
* Uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config
*/
const XDG_CONFIG_DIR = xdgConfig ?? `${USER_HOME_DIR}/.config`;
/**
* Default Claude data directory path (~/.claude)
* Used as base path for loading usage data from JSONL files
*/
const DEFAULT_CLAUDE_CODE_PATH = ".claude";
/**
* Default Claude data directory path using XDG config directory
* Uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config/claude
*/
const DEFAULT_CLAUDE_CONFIG_PATH = `${XDG_CONFIG_DIR}/claude`;
/**
* Environment variable for specifying multiple Claude data directories
* Supports comma-separated paths for multiple locations
*/
const CLAUDE_CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
/**
* Claude projects directory name within the data directory
* Contains subdirectories for each project with usage data
*/
const CLAUDE_PROJECTS_DIR_NAME = "projects";
/**
* JSONL file glob pattern for finding usage data files
* Used to recursively find all JSONL files in project directories
*/
const USAGE_DATA_GLOB_PATTERN = "**/*.jsonl";
/**
* Default port for MCP server HTTP transport
* Used when no port is specified for MCP server communication
*/
const MCP_DEFAULT_PORT = 8080;
/**
* Default refresh interval in seconds for live monitoring mode
* Used in blocks command for real-time updates
*/
const DEFAULT_REFRESH_INTERVAL_SECONDS = 1;
/**
* Minimum refresh interval in seconds for live monitoring mode
* Prevents too-frequent updates that could impact performance
*/
const MIN_REFRESH_INTERVAL_SECONDS = 1;
/**
* Maximum refresh interval in seconds for live monitoring mode
* Prevents too-slow updates that reduce monitoring effectiveness
*/
const MAX_REFRESH_INTERVAL_SECONDS = 60;
var require_usingCtx = __commonJSMin((exports, module) => {
function _usingCtx() {
var r = "function" == typeof SuppressedError ? SuppressedError : function(r$1, e$1) {
var n$1 = Error();
return n$1.name = "SuppressedError", n$1.error = r$1, n$1.suppressed = e$1, n$1;
}, e = {}, n = [];
function using(r$1, e$1) {
if (null != e$1) {
if (Object(e$1) !== e$1) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
if (r$1) var o = e$1[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
if (void 0 === o && (o = e$1[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r$1)) var t = o;
if ("function" != typeof o) throw new TypeError("Object is not disposable.");
t && (o = function o$1() {
try {
t.call(e$1);
} catch (r$2) {
return Promise.reject(r$2);
}
}), n.push({
v: e$1,
d: o,
a: r$1
});
} else r$1 && n.push({
d: e$1,
a: r$1
});
return e$1;
}
return {
e,
u: using.bind(null, !1),
a: using.bind(null, !0),
d: function d() {
var o, t = this.e, s = 0;
function next() {
for (; o = n.pop();) try {
if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
if (o.d) {
var r$1 = o.d.call(o.v);
if (o.a) return s |= 2, Promise.resolve(r$1).then(next, err);
} else s |= 1;
} catch (r$2) {
return err(r$2);
}
if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
if (t !== e) throw t;
}
function err(n$1) {
return t = t !== e ? new r(n$1, t) : n$1, next();
}
return next();
}
};
}
module.exports = _usingCtx, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var import_usingCtx = __toESM(require_usingCtx(), 1);
/**
* Fetches and caches model pricing information from LiteLLM
* Implements Disposable pattern for automatic resource cleanup
*/
var PricingFetcher = class {
cachedPricing = null;
offline;
/**
* Creates a new PricingFetcher instance
* @param offline - Whether to use pre-fetched pricing data instead of fetching from API
*/
constructor(offline = false) {
this.offline = offline;
}
/**
* Implements Disposable interface for automatic cleanup
*/
[Symbol.dispose]() {
this.clearCache();
}
/**
* Clears the cached pricing data
*/
clearCache() {
this.cachedPricing = null;
}
/**
* Ensures pricing data is loaded, either from cache or by fetching
* @returns Map of model names to pricing information
*/
async ensurePricingLoaded() {
if (this.cachedPricing != null) return this.cachedPricing;
if (this.offline) {
const pricing = new Map(Object.entries({
"claude-instant-1": {
"input_cost_per_token": 163e-8,
"output_cost_per_token": 551e-8
},
"claude-instant-1.2": {
"input_cost_per_token": 163e-9,
"output_cost_per_token": 551e-9
},
"claude-2": {
"input_cost_per_token": 8e-6,
"output_cost_per_token": 24e-6
},
"claude-2.1": {
"input_cost_per_token": 8e-6,
"output_cost_per_token": 24e-6
},
"claude-3-haiku-20240307": {
"input_cost_per_token": 25e-8,
"output_cost_per_token": 125e-8,
"cache_creation_input_token_cost": 3e-7,
"cache_read_input_token_cost": 3e-8
},
"claude-3-5-haiku-20241022": {
"input_cost_per_token": 8e-7,
"output_cost_per_token": 4e-6,
"cache_creation_input_token_cost": 1e-6,
"cache_read_input_token_cost": 8e-8
},
"claude-3-5-haiku-latest": {
"input_cost_per_token": 1e-6,
"output_cost_per_token": 5e-6,
"cache_creation_input_token_cost": 125e-8,
"cache_read_input_token_cost": 1e-7
},
"claude-3-opus-latest": {
"input_cost_per_token": 15e-6,
"output_cost_per_token": 75e-6,
"cache_creation_input_token_cost": 1875e-8,
"cache_read_input_token_cost": 15e-7
},
"claude-3-opus-20240229": {
"input_cost_per_token": 15e-6,
"output_cost_per_token": 75e-6,
"cache_creation_input_token_cost": 1875e-8,
"cache_read_input_token_cost": 15e-7
},
"claude-3-sonnet-20240229": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6
},
"claude-3-5-sonnet-latest": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
},
"claude-3-5-sonnet-20240620": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
},
"claude-opus-4-20250514": {
"input_cost_per_token": 15e-6,
"output_cost_per_token": 75e-6,
"cache_creation_input_token_cost": 1875e-8,
"cache_read_input_token_cost": 15e-7
},
"claude-sonnet-4-20250514": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
},
"claude-4-opus-20250514": {
"input_cost_per_token": 15e-6,
"output_cost_per_token": 75e-6,
"cache_creation_input_token_cost": 1875e-8,
"cache_read_input_token_cost": 15e-7
},
"claude-4-sonnet-20250514": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
},
"claude-3-7-sonnet-latest": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
},
"claude-3-7-sonnet-20250219": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
},
"claude-3-5-sonnet-20241022": {
"input_cost_per_token": 3e-6,
"output_cost_per_token": 15e-6,
"cache_creation_input_token_cost": 375e-8,
"cache_read_input_token_cost": 3e-7
}
}));
this.cachedPricing = pricing;
return pricing;
}
try {
logger.warn("Fetching latest model pricing from LiteLLM...");
const response = await fetch(LITELLM_PRICING_URL);
if (!response.ok) throw new Error(`Failed to fetch pricing data: ${response.statusText}`);
const data = await response.json();
const pricing = /* @__PURE__ */ new Map();
for (const [modelName, modelData] of Object.entries(data)) if (typeof modelData === "object" && modelData !== null) {
const parsed = modelPricingSchema.safeParse(modelData);
if (parsed.success) pricing.set(modelName, parsed.data);
}
this.cachedPricing = pricing;
logger.info(`Loaded pricing for ${pricing.size} models`);
return pricing;
} catch (error) {
logger.error("Failed to fetch model pricing:", error);
throw new Error("Could not fetch model pricing data");
}
}
/**
* Fetches all available model pricing data
* @returns Map of model names to pricing information
*/
async fetchModelPricing() {
return this.ensurePricingLoaded();
}
/**
* Gets pricing information for a specific model with fallback matching
* Tries exact match first, then provider prefixes, then partial matches
* @param modelName - Name of the model to get pricing for
* @returns Model pricing information or null if not found
*/
async getModelPricing(modelName) {
const pricing = await this.ensurePricingLoaded();
const directMatch = pricing.get(modelName);
if (directMatch != null) return directMatch;
const variations = [
modelName,
`anthropic/${modelName}`,
`claude-3-5-${modelName}`,
`claude-3-${modelName}`,
`claude-${modelName}`
];
for (const variant of variations) {
const match = pricing.get(variant);
if (match != null) return match;
}
const lowerModel = modelName.toLowerCase();
for (const [key, value] of pricing) if (key.toLowerCase().includes(lowerModel) || lowerModel.includes(key.toLowerCase())) return value;
return null;
}
/**
* Calculates the cost for given token usage and model
* @param tokens - Token usage breakdown
* @param tokens.input_tokens - Number of input tokens
* @param tokens.output_tokens - Number of output tokens
* @param tokens.cache_creation_input_tokens - Number of cache creation tokens
* @param tokens.cache_read_input_tokens - Number of cache read tokens
* @param modelName - Name of the model used
* @returns Total cost in USD
*/
async calculateCostFromTokens(tokens, modelName) {
const pricing = await this.getModelPricing(modelName);
if (pricing == null) return 0;
return this.calculateCostFromPricing(tokens, pricing);
}
/**
* Calculates cost from token usage and pricing information
* @param tokens - Token usage breakdown
* @param tokens.input_tokens - Number of input tokens
* @param tokens.output_tokens - Number of output tokens
* @param tokens.cache_creation_input_tokens - Number of cache creation tokens
* @param tokens.cache_read_input_tokens - Number of cache read tokens
* @param pricing - Model pricing rates
* @returns Total cost in USD
*/
calculateCostFromPricing(tokens, pricing) {
let cost = 0;
if (pricing.input_cost_per_token != null) cost += tokens.input_tokens * pricing.input_cost_per_token;
if (pricing.output_cost_per_token != null) cost += tokens.output_tokens * pricing.output_cost_per_token;
if (tokens.cache_creation_input_tokens != null && pricing.cache_creation_input_token_cost != null) cost += tokens.cache_creation_input_tokens * pricing.cache_creation_input_token_cost;
if (tokens.cache_read_input_tokens != null && pricing.cache_read_input_token_cost != null) cost += tokens.cache_read_input_tokens * pricing.cache_read_input_token_cost;
return cost;
}
};
export { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, DEBUG_MATCH_THRESHOLD_PERCENT, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, PricingFetcher, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR, __commonJSMin, __require, __toESM, require_usingCtx };