UNPKG

@owloops/claude-powerline

Version:

Beautiful vim-style powerline statusline for Claude Code with real-time usage tracking, git integration, and custom themes

455 lines (414 loc) 12.5 kB
import { debug } from "../utils/logger"; import { get } from "node:https"; import { URL } from "node:url"; import { CacheManager } from "../utils/cache"; export interface ModelPricing { name: string; input: number; cache_write_5m: number; cache_write_1h: number; cache_read: number; output: number; } const OFFLINE_PRICING_DATA: Record<string, ModelPricing> = { "claude-haiku-4-5-20251001": { name: "Claude Haiku 4.5", input: 1.0, output: 5.0, cache_write_5m: 1.25, cache_write_1h: 2.0, cache_read: 0.1, }, "claude-haiku-4-5": { name: "Claude Haiku 4.5", input: 1.0, output: 5.0, cache_write_5m: 1.25, cache_write_1h: 2.0, cache_read: 0.1, }, "claude-opus-4-20250514": { name: "Claude Opus 4", input: 15.0, output: 75.0, cache_write_5m: 18.75, cache_write_1h: 30.0, cache_read: 1.5, }, "claude-opus-4-1": { name: "Claude Opus 4.1", input: 15.0, output: 75.0, cache_write_5m: 18.75, cache_write_1h: 30.0, cache_read: 1.5, }, "claude-opus-4-1-20250805": { name: "Claude Opus 4.1", input: 15.0, output: 75.0, cache_write_5m: 18.75, cache_write_1h: 30.0, cache_read: 1.5, }, "claude-sonnet-4-20250514": { name: "Claude Sonnet 4", input: 3.0, output: 15.0, cache_write_5m: 3.75, cache_write_1h: 6.0, cache_read: 0.3, }, "claude-4-opus-20250514": { name: "Claude 4 Opus", input: 15.0, output: 75.0, cache_write_5m: 18.75, cache_write_1h: 30.0, cache_read: 1.5, }, "claude-4-sonnet-20250514": { name: "Claude 4 Sonnet", input: 3.0, output: 15.0, cache_write_5m: 3.75, cache_write_1h: 6.0, cache_read: 0.3, }, "claude-sonnet-4-5": { name: "Claude Sonnet 4.5", input: 3.0, output: 15.0, cache_write_5m: 3.75, cache_write_1h: 6.0, cache_read: 0.3, }, "claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5", input: 3.0, output: 15.0, cache_write_5m: 3.75, cache_write_1h: 6.0, cache_read: 0.3, }, "claude-opus-4-5": { name: "Claude Opus 4.5", input: 5.0, output: 25.0, cache_write_5m: 6.25, cache_write_1h: 10.0, cache_read: 0.5, }, "claude-opus-4-5-20251101": { name: "Claude Opus 4.5", input: 5.0, output: 25.0, cache_write_5m: 6.25, cache_write_1h: 10.0, cache_read: 0.5, }, "claude-opus-4-6": { name: "Claude Opus 4.6", input: 5.0, output: 25.0, cache_write_5m: 6.25, cache_write_1h: 10.0, cache_read: 0.5, }, "claude-opus-4-6-20260205": { name: "Claude Opus 4.6", input: 5.0, output: 25.0, cache_write_5m: 6.25, cache_write_1h: 10.0, cache_read: 0.5, }, "claude-sonnet-4-6": { name: "Claude Sonnet 4.6", input: 3.0, output: 15.0, cache_write_5m: 3.75, cache_write_1h: 6.0, cache_read: 0.3, }, }; export class PricingService { private static executionCache: Record<string, ModelPricing> | null = null; private static modelPricingCache = new Map<string, ModelPricing>(); private static readonly GITHUB_PRICING_URL = "https://raw.githubusercontent.com/Owloops/claude-powerline/main/pricing.json"; private static async loadDiskCache(): Promise<Record< string, ModelPricing > | null> { const TTL_24H = 24 * 60 * 60 * 1000; const minValidTime = Date.now() - TTL_24H; return (await CacheManager.getUsageCache( "pricing", minValidTime, )) as Record<string, ModelPricing> | null; } private static async saveDiskCache( data: Record<string, ModelPricing>, ): Promise<void> { await CacheManager.setUsageCache("pricing", data); } private static async fetchPricingData(): Promise<Record< string, ModelPricing > | null> { return new Promise((resolve) => { const parsedUrl = new URL(this.GITHUB_PRICING_URL); const request = get( { hostname: parsedUrl.hostname, path: parsedUrl.pathname, headers: { "User-Agent": "claude-powerline", "Cache-Control": "no-cache", }, timeout: 5000, }, (response) => { if (response.statusCode !== 200) { debug(`HTTP ${response.statusCode}: ${response.statusMessage}`); resolve(null); return; } let data = ""; let size = 0; const MAX_SIZE = 1024 * 1024; response.on("data", (chunk) => { size += chunk.length; if (size > MAX_SIZE) { debug("Response too large"); request.destroy(); resolve(null); return; } data += chunk; }); response.on("end", () => { try { const json = JSON.parse(data); const dataObj = json as Record<string, unknown>; const meta = dataObj._meta as { updated?: string } | undefined; const pricingData: Record<string, unknown> = {}; for (const [key, value] of Object.entries(dataObj)) { if (key !== "_meta") { pricingData[key] = value; } } if (this.validatePricingData(pricingData)) { debug( `Fetched fresh pricing from GitHub for ${Object.keys(pricingData).length} models`, ); debug(`Pricing last updated: ${meta?.updated || "unknown"}`); resolve(pricingData); } else { debug("Invalid pricing data structure"); resolve(null); } } catch (error) { debug("Failed to parse JSON:", error); resolve(null); } }); response.on("error", (error) => { debug("Response error:", error); resolve(null); }); }, ); request.on("error", (error) => { debug("Request error:", error); resolve(null); }); request.on("timeout", () => { debug("Request timeout"); request.destroy(); resolve(null); }); request.end(); }); } static async getCurrentPricing(): Promise<Record<string, ModelPricing>> { if (this.executionCache !== null) { debug( `[CACHE-HIT] Pricing execution cache: ${Object.keys(this.executionCache).length} models`, ); return this.executionCache; } const diskCached = await this.loadDiskCache(); if (diskCached) { debug( `[CACHE-HIT] Pricing disk cache: ${Object.keys(diskCached).length} models`, ); this.executionCache = diskCached; debug( `[CACHE-SET] Pricing execution cache stored: ${Object.keys(diskCached).length} models`, ); return diskCached; } const freshData = await this.fetchPricingData(); if (freshData) { await this.saveDiskCache(freshData); debug( `[CACHE-SET] Pricing disk cache stored: ${Object.keys(freshData).length} models`, ); this.executionCache = freshData; debug( `[CACHE-SET] Pricing execution cache stored: ${Object.keys(freshData).length} models`, ); return freshData; } debug( `[CACHE-FALLBACK] Using offline pricing data: ${Object.keys(OFFLINE_PRICING_DATA).length} models`, ); this.executionCache = OFFLINE_PRICING_DATA; debug( `[CACHE-SET] Pricing execution cache stored: ${Object.keys(OFFLINE_PRICING_DATA).length} models`, ); return OFFLINE_PRICING_DATA; } private static validatePricingData( data: unknown, ): data is Record<string, ModelPricing> { if (!data || typeof data !== "object") return false; for (const [, value] of Object.entries(data)) { if (!value || typeof value !== "object") return false; const pricing = value as Record<string, unknown>; if ( typeof pricing.input !== "number" || typeof pricing.output !== "number" || typeof pricing.cache_read !== "number" ) { return false; } } return true; } static async getModelPricing(modelId: string): Promise<ModelPricing> { if (this.modelPricingCache.has(modelId)) { debug(`[CACHE-HIT] Model pricing cache: ${modelId}`); return this.modelPricingCache.get(modelId)!; } const allPricing = await this.getCurrentPricing(); let pricing: ModelPricing; if (allPricing[modelId]) { pricing = allPricing[modelId]; } else { pricing = this.fuzzyMatchModel(modelId, allPricing); } this.modelPricingCache.set(modelId, pricing); debug(`[CACHE-SET] Model pricing cache: ${modelId}`); return pricing; } private static fuzzyMatchModel( modelId: string, allPricing: Record<string, ModelPricing>, ): ModelPricing { const lowerModelId = modelId.toLowerCase(); for (const [key, pricing] of Object.entries(allPricing)) { if (key.toLowerCase() === lowerModelId) { return pricing; } } const patterns = [ { pattern: ["opus-4-6", "claude-opus-4-6"], fallback: "claude-opus-4-6-20260205", }, { pattern: ["opus-4-5", "claude-opus-4-5"], fallback: "claude-opus-4-5-20251101", }, { pattern: ["opus-4-1", "claude-opus-4-1"], fallback: "claude-opus-4-1-20250805", }, { pattern: ["opus-4", "claude-opus-4"], fallback: "claude-opus-4-20250514", }, { pattern: ["sonnet-4-6", "sonnet-4.6", "claude-sonnet-4-6"], fallback: "claude-sonnet-4-6", }, { pattern: ["sonnet-4.5", "4-5-sonnet", "sonnet-4-5"], fallback: "claude-sonnet-4-5-20250929", }, { pattern: ["sonnet-4", "claude-sonnet-4"], fallback: "claude-sonnet-4-20250514", }, { pattern: ["haiku-4.5", "4-5-haiku", "haiku-4-5"], fallback: "claude-haiku-4-5-20251001", }, { pattern: ["haiku"], fallback: "claude-haiku-4-5-20251001" }, { pattern: ["opus"], fallback: "claude-opus-4-20250514" }, { pattern: ["sonnet"], fallback: "claude-sonnet-4-5-20250929" }, ]; for (const { pattern, fallback } of patterns) { if (pattern.some((p) => lowerModelId.includes(p))) { if (allPricing[fallback]) { return allPricing[fallback]; } } } return ( allPricing["claude-sonnet-4-5-20250929"] || { name: `${modelId} (Unknown Model)`, input: 3.0, cache_write_5m: 3.75, cache_write_1h: 6.0, cache_read: 0.3, output: 15.0, } ); } static async calculateCostForEntry( entry: Record<string, unknown>, ): Promise<number> { const message = entry.message as Record<string, unknown> | undefined; const usage = message?.usage as Record<string, number> | undefined; if (!usage) { return 0; } const modelId = this.extractModelId(entry); const pricing = await this.getModelPricing(modelId); const inputTokens = usage.input_tokens || 0; const outputTokens = usage.output_tokens || 0; const cacheCreationTokens = usage.cache_creation_input_tokens || 0; const cacheReadTokens = usage.cache_read_input_tokens || 0; const inputCost = (inputTokens / 1_000_000) * pricing.input; const outputCost = (outputTokens / 1_000_000) * pricing.output; const cacheReadCost = (cacheReadTokens / 1_000_000) * pricing.cache_read; const cacheCreationCost = (cacheCreationTokens / 1_000_000) * pricing.cache_write_5m; return inputCost + outputCost + cacheCreationCost + cacheReadCost; } private static extractModelId(entry: Record<string, unknown>): string { if (entry.model && typeof entry.model === "string") { return entry.model; } const message = entry.message as Record<string, unknown> | undefined; if (message?.model) { const model = message.model; if (typeof model === "string") { return model; } const modelObj = model as Record<string, unknown> | undefined; return ( (typeof modelObj?.id === "string" ? modelObj.id : null) || "claude-sonnet-4-5-20250929" ); } if (entry.model_id && typeof entry.model_id === "string") { return entry.model_id; } return "claude-sonnet-4-5-20250929"; } }