diff --git a/AGENTS.md b/AGENTS.md index 7b45dafa..e7f02fef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,7 @@ npm start build-cmd # tsc --project etc/tsc.json (emits into dst/) npm start build-plugin # copy ../plugin and ../.claude-plugin into the package npm start build-watch # nodemon rebuild on src/**/*.ts npm start lint-watch # nodemon relint on src/**/*.ts +npm start prices-update # refresh checked-in LiteLLM price snapshot (needs network) cd pages npm start lint # astro check + eslint over src/**/*.{ts,astro} diff --git a/docs/usage-tool.md b/docs/usage-tool.md index 503a9f80..ffdb63e8 100644 --- a/docs/usage-tool.md +++ b/docs/usage-tool.md @@ -177,7 +177,7 @@ background service as an *Anthropic Claude Code CLI* MCP server: The following top-level command exists for rendering the *Anthropic Claude Code CLI* or *GitHub Copilot CLI* statusline: -- `ase statusline` \[`-t`|`--tool` `claude`|`copilot`\] \[`-w`|`--width` *n*\] \[`-m`|`--margin` *n*\] \[`-p`|`--padding` *n*\] \[`--no-icons`\] \[`--no-labels`\] \[*line* \[...\]\]: +- `ase statusline` \[`-t`|`--tool` `claude`|`copilot`\] \[`-w`|`--width` *n*\] \[`-m`|`--margin` *n*\] \[`-p`|`--padding` *n*\] \[`--no-icons`\] \[`--no-labels`\] \[`--month-cost-ttl` *n*\] \[*line* \[...\]\]: Render the *Anthropic Claude Code CLI* or *GitHub Copilot CLI* statusline from a JSON payload read on standard input. Intended to be configured as the `statusLine` command in *Anthropic Claude Code CLI* settings (or the @@ -216,7 +216,9 @@ or *GitHub Copilot CLI* statusline: e.g. `4hr 27m`), `%W` (7-day rate-limit window used percentage), `%Q` (7-day window time-until-reset), `%H` (session wall-clock duration, e.g. `92hr 40m`), `%X` (session cost in USD, e.g. - `$54.44`), `%b` (git branch, or `no git`), `%g` (git changed lines, + `$54.44`), `%Y` (cumulative cost in USD across *all* sessions of + *all* supported agent tools within the current calendar month, e.g. + `$1102.11`), `%b` (git branch, or `no git`), `%g` (git changed lines, e.g. `+42/-7`), `%G` (git untracked file count), `%d` (full current working directory path), `%M` (memory used/total, e.g. `33.2G/64.0G`), `%V` (combined *Anthropic Claude Code CLI* and *ASE* @@ -259,7 +261,32 @@ or *GitHub Copilot CLI* statusline: - \[`--no-labels`\]: disable the textual label (e.g. `user:`, `project:`, `model:`) in front of the bold value of each placeholder rendering. - When run inside a *tmux* pane, the resolved task id is also + - \[`--month-cost-ttl` *n*\]: + seconds the `%Y` current-month total cost is cached before a + non-blocking background refresh is triggered (default: `300`). + The `%Y` placeholder reports the cumulative cost (in USD) of *all* + agent sessions within the current calendar month, unlike `%X` which + only reflects the current session. The month boundary is a *UTC* one, + matching the day on which the model vendors bill and reset their + usage windows. The figure is computed locally, without any network + access, from the session logs of every supported agent tool: + *Anthropic Claude Code CLI* (`~/.claude/projects/**/*.jsonl`, honoring + `CLAUDE_CONFIG_DIR`), *OpenAI Codex CLI* + (`~/.codex/{sessions,archived_sessions}/**/rollout-*.jsonl`, honoring + `CODEX_HOME`), and *GitHub Copilot CLI* + (`~/.copilot/session-state/**/*.jsonl`, honoring + `COPILOT_CONFIG_DIR`). Per logged model call, the token counts + (uncached input, output including reasoning, cache-read, and + 5-minute / 1-hour cache-write) are multiplied by the per-model prices + of the *LiteLLM* price snapshot bundled with *ASE* (see `npm start + prices-update`); a call logged more than once - while its response + streams, or after a session was resumed or forked - is billed only + once, and a model absent from the snapshot contributes nothing. + To keep rendering fast, the result is cached in the temporary + directory and recomputed at most once per *--month-cost-ttl* window + by a detached background process, so a render never blocks on the + log scan; missing or empty logs simply suppress the + placeholder. When run inside a *tmux* pane, the resolved task id is also published as the per-pane user option `@ase_task_id`, so external tools (like the *claudeX* sister project) can pick it up via `#{@ase_task_id}`. diff --git a/tool/etc/litellm-prices.mjs b/tool/etc/litellm-prices.mjs new file mode 100644 index 00000000..76641b82 --- /dev/null +++ b/tool/etc/litellm-prices.mjs @@ -0,0 +1,94 @@ +/* +** Agentic Software Engineering (ASE) +** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall +** Licensed under Apache 2.0 +*/ + +/* Regenerate the per-model token price snapshot in "src/ase-statusline-prices.ts" + from LiteLLM's canonical price database. Run via "npm start prices-update". + The snapshot is checked in on purpose, so that both the build and the + statusline rendering stay entirely offline. */ + +import fs from "node:fs" +import path from "node:path" +import url from "node:url" + +/* canonical upstream price database (the same source ccusage and codeburn use) */ +const SOURCE = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" + +/* LiteLLM providers whose models can show up in the session logs of the + supported agent tools (Anthropic Claude Code, OpenAI Codex CLI, and + GitHub Copilot CLI, which brokers models of several vendors) */ +const PROVIDERS = new Set([ "anthropic", "openai", "gemini", "xai", "deepseek", "mistral" ]) + +/* LiteLLM modes that bill input/output tokens of a conversation */ +const MODES = new Set([ "chat", "responses" ]) + +const main = async () => { + const res = await fetch(SOURCE) + if (!res.ok) + throw new Error(`fetching ${SOURCE} failed: ${res.status} ${res.statusText}`) + const db = await res.json() + + /* reduce the database to the token prices of the relevant models, keying + them by their bare model id: LiteLLM prefixes most non-OpenAI models + with their provider ("gemini/gemini-2.5-pro"), while the agent tools + log the bare id. An already bare entry always wins over a prefixed + one, so that the canonical price is never shadowed. */ + const prices = new Map() + for (const [ id, spec ] of Object.entries(db)) { + if (typeof spec !== "object" || spec === null) + continue + if (!PROVIDERS.has(spec.litellm_provider) || !MODES.has(spec.mode)) + continue + const input = spec.input_cost_per_token + const output = spec.output_cost_per_token + if (typeof input !== "number" || typeof output !== "number") + continue + + /* cache-read defaults to the regular input price (a model without + prompt caching never reports cached tokens anyway), while a + missing cache-write price means writing is not billed at all */ + const cacheRead = typeof spec.cache_read_input_token_cost === "number" ? + spec.cache_read_input_token_cost : input + const cacheWrite = typeof spec.cache_creation_input_token_cost === "number" ? + spec.cache_creation_input_token_cost : 0 + const cacheWrite1 = typeof spec.cache_creation_input_token_cost_above_1hr === "number" ? + spec.cache_creation_input_token_cost_above_1hr : cacheWrite + + const bare = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id + const prefix = id.includes("/") + if (prices.has(bare) && prefix) + continue + prices.set(bare, [ input, output, cacheRead, cacheWrite, cacheWrite1 ]) + } + + const ids = [ ...prices.keys() ].sort() + const lines = ids.map((id) => ` ${JSON.stringify(id)}: ${ + JSON.stringify(prices.get(id)).replace(/,/g, ", ").replace(/^\[/, "[ ").replace(/\]$/, " ]")}`) + + const out = `/* +** Agentic Software Engineering (ASE) +** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall +** Licensed under GPL 3.0 +*/ + +/* GENERATED FILE -- do NOT edit manually. + Regenerate with "npm start prices-update" (see etc/litellm-prices.mjs). + Source: ${SOURCE} */ + +/* per-model token prices in USD per single token, as the tuple + [ input, output, cache-read, cache-write (5m), cache-write (1h) ] */ +export type Price = readonly [ number, number, number, number, number ] + +export const prices: Readonly> = { +${lines.join(",\n")} +} +` + const dir = path.dirname(url.fileURLToPath(import.meta.url)) + const file = path.resolve(dir, "..", "src", "ase-statusline-prices.ts") + fs.writeFileSync(file, out, "utf8") + process.stdout.write(`ase: prices-update: wrote ${ids.length} model prices to ${file}\n`) +} + +await main() diff --git a/tool/etc/stx.conf b/tool/etc/stx.conf index 04d934dd..c8240e5e 100644 --- a/tool/etc/stx.conf +++ b/tool/etc/stx.conf @@ -16,6 +16,10 @@ lint-watch build-watch nodemon --exec "npm start build" --watch src --ext ts +# [tool] refresh the checked-in per-model token price snapshot (requires network) +prices-update + node etc/litellm-prices.mjs + # [tool] build entire project build : lint build-cmd build-plugin diff --git a/tool/src/ase-statusline-cost.ts b/tool/src/ase-statusline-cost.ts new file mode 100644 index 00000000..503ff792 --- /dev/null +++ b/tool/src/ase-statusline-cost.ts @@ -0,0 +1,385 @@ +/* +** Agentic Software Engineering (ASE) +** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall +** Licensed under GPL 3.0 +*/ + +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { spawn } from "node:child_process" + +import { prices } from "./ase-statusline-prices.js" +import type { Price } from "./ase-statusline-prices.js" + +/* on-disk cache shape for the cumulative current-month cost */ +export interface MonthCostCache { + version: number /* computation scheme, to invalidate results of an older ASE */ + month: string /* "YYYY-MM" in UTC */ + costUsd: number /* cumulative cost across all sessions of all agent tools */ + computedAt: number /* epoch milliseconds of the last computation */ +} + +/* current computation scheme: bump whenever the scanning, pricing, or + month-bucketing semantics change, so that a cache written by an older + ASE is discarded instead of being rendered as if it were current */ +const SCHEME = 2 + +/* normalized token usage of a single billed model call */ +interface Usage { + input: number /* uncached input tokens */ + output: number /* generated tokens, including reasoning tokens */ + cacheRead: number /* prompt-cache read tokens */ + cacheWrite5m: number /* prompt-cache write tokens with the 5-minute TTL */ + cacheWrite1h: number /* prompt-cache write tokens with the 1-hour TTL */ +} + +/* a single billed model call, as reconstructed from an agent tool session log */ +interface Call { + key: string /* stable identity, so that a call logged more than once is billed once */ + model: string + usage: Usage +} + +const noUsage = (): Usage => ({ input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0 }) + +/* resolve the token prices of a model id. The agent tools log ids in + varying shapes, so the lookup widens step by step: the bare id, the id + with dots normalized to dashes (Copilot renders "claude-sonnet-4.5"), + the id without its vendor prefix ("anthropic/claude-opus-5"), and + finally the longest matching id prefix, which maps dated snapshots + like "claude-opus-5-20260401" onto their base entry. A model that + remains unknown contributes nothing, since it cannot be priced. */ +const resolved = new Map() +const resolvePrice = (model: string): Price | null => { + const memo = resolved.get(model) + if (memo !== undefined) + return memo + const price = resolvePriceUncached(model) + resolved.set(model, price) + return price +} +const resolvePriceUncached = (model: string): Price | null => { + const candidates = [ model, model.replace(/\./g, "-") ] + if (model.includes("/")) + candidates.push(model.slice(model.indexOf("/") + 1)) + for (const candidate of candidates) + if (prices[candidate] !== undefined) + return prices[candidate]! + let best: string | null = null + for (const candidate of candidates) + for (const id of Object.keys(prices)) + if (candidate.startsWith(id) && (best === null || id.length > best.length)) + best = id + return best !== null ? prices[best]! : null +} + +/* price a single call, returning 0 for a model without known prices */ +const costOf = (call: Call): number => { + const price = resolvePrice(call.model) + if (price === null) + return 0 + const [ input, output, cacheRead, cacheWrite5m, cacheWrite1h ] = price + return call.usage.input * input + + call.usage.output * output + + call.usage.cacheRead * cacheRead + + call.usage.cacheWrite5m * cacheWrite5m + + call.usage.cacheWrite1h * cacheWrite1h +} + +/* derive the "YYYY-MM" key of a date in UTC: the agent vendors bill and + reset their usage windows on UTC days, so a local-time month boundary + would attribute the calls of a late evening to the wrong month */ +const monthKeyOf = (d: Date): string => { + const y = d.getUTCFullYear() + const m = d.getUTCMonth() + 1 + return `${y}-${m < 10 ? "0" : ""}${m}` +} + +/* first millisecond of the UTC month a date falls into */ +const startOfMonth = (d: Date): number => + Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1) + +/* parse a timestamp of any shape the session logs use (ISO 8601 string or + epoch seconds/milliseconds), returning null when it is unusable */ +const parseTime = (raw: unknown): Date | null => { + if (typeof raw === "number") { + const d = new Date(raw < 1e12 ? raw * 1000 : raw) + return Number.isNaN(d.getTime()) ? null : d + } + if (typeof raw === "string" && raw !== "") { + const d = new Date(raw) + return Number.isNaN(d.getTime()) ? null : d + } + return null +} + +/* resolve a configuration root directory, honoring an environment override */ +const rootDir = (envVar: string, fallback: string): string => { + const env = process.env[envVar] + if (env !== undefined && env.trim() !== "") + return env.trim() + return path.join(os.homedir(), fallback) +} + +/* recursively yield every file below a directory whose name passes a filter */ +function * filesBelow (dir: string, accept: (name: string) => boolean): Generator { + let entries: fs.Dirent[] + try { + entries = fs.readdirSync(dir, { withFileTypes: true }) + } + catch (_e) { + return + } + for (const ent of entries) { + const p = path.join(dir, ent.name) + if (ent.isDirectory()) + yield * filesBelow(p, accept) + else if (ent.isFile() && accept(ent.name)) + yield p + } +} + +/* read a session log as parsed JSONL records, skipping whole files that were + not touched within the month (they cannot hold a current-month call, which + keeps the scan off the entire archive) and tolerating malformed lines */ +function * records (file: string, since: number): Generator<{ obj: any, line: number }> { + try { + if (fs.statSync(file).mtimeMs < since) + return + } + catch (_e) { + return + } + let lines: string[] + try { + lines = fs.readFileSync(file, "utf8").split("\n") + } + catch (_e) { + return + } + for (let i = 0; i < lines.length; i++) { + if (lines[i] === "") + continue + try { + yield { obj: JSON.parse(lines[i]!), line: i } + } + catch (_e) { + /* a truncated trailing line of a live session is expected */ + } + } +} + +/* scan the session logs of Anthropic Claude Code. Every assistant record + carries the usage of exactly one API call, but the same call is written + repeatedly while its response streams and again when a session is resumed + or forked, so the message id keys the de-duplication. */ +function * scanClaude (month: string, since: number): Generator { + const root = path.join(rootDir("CLAUDE_CONFIG_DIR", ".claude"), "projects") + for (const file of filesBelow(root, (n) => n.endsWith(".jsonl"))) { + for (const { obj, line } of records(file, since)) { + const usage = obj?.message?.usage + if (usage === undefined || usage === null) + continue + const at = parseTime(obj?.timestamp) + if (at === null || monthKeyOf(at) !== month) + continue + const u = noUsage() + u.input = usage.input_tokens ?? 0 + u.output = usage.output_tokens ?? 0 + u.cacheRead = usage.cache_read_input_tokens ?? 0 + const c5 = usage.cache_creation?.ephemeral_5m_input_tokens + const c1 = usage.cache_creation?.ephemeral_1h_input_tokens + if (c5 !== undefined || c1 !== undefined) { + u.cacheWrite5m = c5 ?? 0 + u.cacheWrite1h = c1 ?? 0 + } + else + u.cacheWrite5m = usage.cache_creation_input_tokens ?? 0 + const id = typeof obj?.message?.id === "string" ? obj.message.id : "" + yield { + key: id !== "" ? `claude:${id}` : `claude:${file}:${line}`, + model: typeof obj?.message?.model === "string" ? obj.message.model : "", + usage: u + } + } + } +} + +/* scan the rollout logs of the OpenAI Codex CLI. Codex reports the usage of + a call in a dedicated "token_count" event whose "last_token_usage" holds + the delta of that very call, so the events are summed as they come and + keyed positionally. The model is carried by the session and turn headers + rather than by the usage event itself. */ +function * scanCodex (month: string, since: number): Generator { + const root = rootDir("CODEX_HOME", ".codex") + for (const dir of [ path.join(root, "sessions"), path.join(root, "archived_sessions") ]) { + for (const file of filesBelow(dir, (n) => n.startsWith("rollout-") && n.endsWith(".jsonl"))) { + let model = "gpt-5" + for (const { obj, line } of records(file, since)) { + const payload = obj?.payload + const named = payload?.model ?? payload?.info?.model ?? payload?.info?.model_name + if (typeof named === "string" && named !== "") + model = named + if (obj?.type !== "event_msg" || payload?.type !== "token_count") + continue + const usage = payload?.info?.last_token_usage + if (usage === undefined || usage === null) + continue + const at = parseTime(obj?.timestamp) + if (at === null || monthKeyOf(at) !== month) + continue + + /* Codex counts the cached tokens within its input total, + so the uncached remainder has to be recovered */ + const cached = usage.cached_input_tokens ?? 0 + const u = noUsage() + u.input = Math.max(0, (usage.input_tokens ?? 0) - cached) + u.output = (usage.output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0) + u.cacheRead = cached + yield { key: `codex:${file}:${line}`, model, usage: u } + } + } + } +} + +/* scan the session state of the GitHub Copilot CLI. Copilot aggregates the + usage per model over the whole session, so a single record yields one + entry per involved model, and its input total covers the cached tokens as + well. Since that aggregate is cumulative, the entries of one session and + model are keyed identically, which bills only the final one. */ +function * scanCopilot (month: string, since: number): Generator { + const root = path.join(rootDir("COPILOT_CONFIG_DIR", ".copilot"), "session-state") + for (const file of filesBelow(root, (n) => n.endsWith(".jsonl"))) { + for (const { obj } of records(file, since)) { + const metrics = obj?.modelMetrics + if (typeof metrics !== "object" || metrics === null) + continue + const at = parseTime(obj?.timestamp) + if (at === null || monthKeyOf(at) !== month) + continue + for (const [ model, entry ] of Object.entries(metrics)) { + const usage = entry?.usage ?? entry + if (typeof usage !== "object" || usage === null) + continue + const cacheRead = usage.cacheReadTokens ?? 0 + const cacheWrite = usage.cacheWriteTokens ?? 0 + const u = noUsage() + u.input = Math.max(0, (usage.inputTokens ?? 0) - cacheRead - cacheWrite) + u.output = (usage.outputTokens ?? 0) + (usage.reasoningTokens ?? 0) + u.cacheRead = cacheRead + u.cacheWrite5m = cacheWrite + yield { key: `copilot:${file}:${model}`, model, usage: u } + } + } + } +} + +/* all supported agent tools, scanned in one pass */ +const scanners = [ scanClaude, scanCodex, scanCopilot ] + +/* per-user cache file in the temporary directory */ +const cacheFile = (): string => { + let user: string + try { + user = os.userInfo().username || "default" + } + catch (_e) { + user = process.env.USER ?? "default" + } + return path.join(os.tmpdir(), `ase-statusline-month-cost-${user}.json`) +} + +/* read the persisted month-cost cache, or null when absent, unreadable, or + written by an ASE with a different computation scheme */ +export const readMonthCostCache = (): MonthCostCache | null => { + try { + const obj = JSON.parse(fs.readFileSync(cacheFile(), "utf8")) as MonthCostCache + if (obj.version === SCHEME + && typeof obj.month === "string" + && typeof obj.costUsd === "number" + && typeof obj.computedAt === "number") + return obj + return null + } + catch (_e) { + return null + } +} + +const writeMonthCostCache = (cache: MonthCostCache): void => { + try { + fs.writeFileSync(cacheFile(), JSON.stringify(cache), "utf8") + } + catch (_e) { + /* best-effort: a non-writable temp directory just means no caching */ + } +} + +/* scan the local session logs of every supported agent tool and sum the cost + of all calls billed within the given UTC month. Calls that were logged + more than once are billed once, keeping the most expensive of their + snapshots: the usage counts of a call grow while its response streams, so + the largest snapshot reflects the finally billed state. Missing or + unreadable logs simply contribute 0 without throwing. */ +export const computeMonthCost = (now: Date): number => { + const month = monthKeyOf(now) + const since = startOfMonth(now) + const seen = new Map() + for (const scan of scanners) { + for (const call of scan(month, since)) { + const cost = costOf(call) + if (cost > (seen.get(call.key) ?? -1)) + seen.set(call.key, cost) + } + } + let total = 0 + for (const cost of seen.values()) + total += cost + return total +} + +/* recompute the current-month cost and persist it to the cache file */ +export const refreshMonthCostCache = (now: Date): void => { + writeMonthCostCache({ + version: SCHEME, + month: monthKeyOf(now), + costUsd: computeMonthCost(now), + computedAt: now.getTime() + }) +} + +/* spawn a detached background process that recomputes the cache without + blocking the current statusline render; any failure is swallowed since a + missed refresh only means the next render keeps using the stale value */ +const spawnMonthCostRefresh = (): void => { + try { + const entry = process.argv[1] + if (entry === undefined) + return + const child = spawn(process.execPath, [ entry, "statusline", "--refresh-month-cost" ], + { detached: true, stdio: "ignore" }) + child.unref() + } + catch (_e) { + /* unable to spawn: keep serving the last cached value */ + } +} + +/* resolve the value to render for the %Y current-month cost placeholder: + returns the cached cost when it is for the current month, and triggers a + non-blocking background refresh whenever the cache is missing, stale + (older than ttlSec), or from a previous month. Returns null when there is + no usable current-month value yet (first run, or no logged usage). */ +export const monthCostForRender = (now: Date, ttlSec: number): number | null => { + const month = monthKeyOf(now) + const cache = readMonthCostCache() + const fresh = cache !== null + && cache.month === month + && now.getTime() - cache.computedAt < ttlSec * 1000 + if (!fresh) + spawnMonthCostRefresh() + if (cache !== null && cache.month === month && cache.costUsd > 0) + return cache.costUsd + return null +} diff --git a/tool/src/ase-statusline-prices.ts b/tool/src/ase-statusline-prices.ts new file mode 100644 index 00000000..696451a7 --- /dev/null +++ b/tool/src/ase-statusline-prices.ts @@ -0,0 +1,289 @@ +/* +** Agentic Software Engineering (ASE) +** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall +** Licensed under GPL 3.0 +*/ + +/* GENERATED FILE -- do NOT edit manually. + Regenerate with "npm start prices-update" (see etc/litellm-prices.mjs). + Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json */ + +/* per-model token prices in USD per single token, as the tuple + [ input, output, cache-read, cache-write (5m), cache-write (1h) ] */ +export type Price = readonly [ number, number, number, number, number ] + +export const prices: Readonly> = { + "chatgpt-4o-latest": [ 0.000005, 0.000015, 0.000005, 0, 0 ], + "claude-3-7-sonnet-20250219": [ 0.000003, 0.000015, 3e-7, 0.00000375, 0.000006 ], + "claude-3-haiku-20240307": [ 2.5e-7, 0.00000125, 3e-8, 3e-7, 0.000006 ], + "claude-3-opus-20240229": [ 0.000015, 0.000075, 0.0000015, 0.00001875, 0.000006 ], + "claude-4-opus-20250514": [ 0.000015, 0.000075, 0.0000015, 0.00001875, 0.00001875 ], + "claude-4-sonnet-20250514": [ 0.000003, 0.000015, 3e-7, 0.00000375, 0.00000375 ], + "claude-fable-5": [ 0.00001, 0.00005, 0.000001, 0.0000125, 0.00002 ], + "claude-haiku-4-5": [ 0.000001, 0.000005, 1e-7, 0.00000125, 0.000002 ], + "claude-haiku-4-5-20251001": [ 0.000001, 0.000005, 1e-7, 0.00000125, 0.000002 ], + "claude-opus-4-1": [ 0.000015, 0.000075, 0.0000015, 0.00001875, 0.00003 ], + "claude-opus-4-1-20250805": [ 0.000015, 0.000075, 0.0000015, 0.00001875, 0.00003 ], + "claude-opus-4-20250514": [ 0.000015, 0.000075, 0.0000015, 0.00001875, 0.00003 ], + "claude-opus-4-5": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-4-5-20251101": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-4-6": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-4-6-20260205": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-4-7": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-4-7-20260416": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-4-8": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-opus-5": [ 0.000005, 0.000025, 5e-7, 0.00000625, 0.00001 ], + "claude-sonnet-4-20250514": [ 0.000003, 0.000015, 3e-7, 0.00000375, 0.000006 ], + "claude-sonnet-4-5": [ 0.000003, 0.000015, 3e-7, 0.00000375, 0.000006 ], + "claude-sonnet-4-5-20250929": [ 0.000003, 0.000015, 3e-7, 0.00000375, 0.000006 ], + "claude-sonnet-4-6": [ 0.000003, 0.000015, 3e-7, 0.00000375, 0.000006 ], + "claude-sonnet-5": [ 0.000002, 0.00001, 2e-7, 0.0000025, 0.000004 ], + "codestral-2405": [ 0.000001, 0.000003, 0.000001, 0, 0 ], + "codestral-2508": [ 3e-7, 9e-7, 3e-7, 0, 0 ], + "codestral-latest": [ 0.000001, 0.000003, 0.000001, 0, 0 ], + "codestral-mamba-latest": [ 2.5e-7, 2.5e-7, 2.5e-7, 0, 0 ], + "codex-mini-latest": [ 0.0000015, 0.000006, 3.75e-7, 0, 0 ], + "deepseek-chat": [ 2.8e-7, 4.2e-7, 2.8e-8, 0, 0 ], + "deepseek-coder": [ 1.4e-7, 2.8e-7, 1.4e-7, 0, 0 ], + "deepseek-r1": [ 5.5e-7, 0.00000219, 5.5e-7, 0, 0 ], + "deepseek-reasoner": [ 2.8e-7, 4.2e-7, 2.8e-8, 0, 0 ], + "deepseek-v3": [ 2.7e-7, 0.0000011, 7e-8, 0, 0 ], + "deepseek-v3.2": [ 2.8e-7, 4e-7, 2.8e-7, 0, 0 ], + "deepseek-v4-flash": [ 1.4e-7, 2.8e-7, 2.8e-9, 0, 0 ], + "deepseek-v4-pro": [ 4.35e-7, 8.7e-7, 3.625e-9, 0, 0 ], + "devstral-2512": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "devstral-latest": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "devstral-medium-2507": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "devstral-medium-latest": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "devstral-small-2505": [ 1e-7, 3e-7, 1e-7, 0, 0 ], + "devstral-small-2507": [ 1e-7, 3e-7, 1e-7, 0, 0 ], + "devstral-small-latest": [ 1e-7, 3e-7, 1e-7, 0, 0 ], + "ft:gpt-3.5-turbo": [ 0.000003, 0.000006, 0.000003, 0, 0 ], + "ft:gpt-3.5-turbo-0125": [ 0.000003, 0.000006, 0.000003, 0, 0 ], + "ft:gpt-3.5-turbo-0613": [ 0.000003, 0.000006, 0.000003, 0, 0 ], + "ft:gpt-3.5-turbo-1106": [ 0.000003, 0.000006, 0.000003, 0, 0 ], + "ft:gpt-4-0613": [ 0.00003, 0.00006, 0.00003, 0, 0 ], + "ft:gpt-4.1-2025-04-14": [ 0.000003, 0.000012, 7.5e-7, 0, 0 ], + "ft:gpt-4.1-mini-2025-04-14": [ 8e-7, 0.0000032, 2e-7, 0, 0 ], + "ft:gpt-4.1-nano-2025-04-14": [ 2e-7, 8e-7, 5e-8, 0, 0 ], + "ft:gpt-4o-2024-08-06": [ 0.00000375, 0.000015, 0.000001875, 0, 0 ], + "ft:gpt-4o-2024-11-20": [ 0.00000375, 0.000015, 0.00000375, 0.000001875, 0.000001875 ], + "ft:gpt-4o-mini-2024-07-18": [ 3e-7, 0.0000012, 1.5e-7, 0, 0 ], + "ft:o4-mini-2025-04-16": [ 0.000004, 0.000016, 0.000001, 0, 0 ], + "gemini-2.0-flash": [ 1e-7, 4e-7, 2.5e-8, 0, 0 ], + "gemini-2.0-flash-001": [ 1e-7, 4e-7, 2.5e-8, 0, 0 ], + "gemini-2.0-flash-lite": [ 7.5e-8, 3e-7, 1.875e-8, 0, 0 ], + "gemini-2.0-flash-lite-001": [ 7.5e-8, 3e-7, 1.875e-8, 0, 0 ], + "gemini-2.5-computer-use-preview-10-2025": [ 0.00000125, 0.00001, 0.00000125, 0, 0 ], + "gemini-2.5-flash": [ 3e-7, 0.0000025, 3e-8, 0, 0 ], + "gemini-2.5-flash-lite": [ 1e-7, 4e-7, 1e-8, 0, 0 ], + "gemini-2.5-flash-lite-preview-06-17": [ 1e-7, 4e-7, 2.5e-8, 0, 0 ], + "gemini-2.5-flash-lite-preview-09-2025": [ 1e-7, 4e-7, 1e-8, 0, 0 ], + "gemini-2.5-flash-native-audio-latest": [ 3e-7, 0.0000025, 3e-7, 0, 0 ], + "gemini-2.5-flash-native-audio-preview-09-2025": [ 3e-7, 0.0000025, 3e-7, 0, 0 ], + "gemini-2.5-flash-native-audio-preview-12-2025": [ 3e-7, 0.0000025, 3e-7, 0, 0 ], + "gemini-2.5-flash-preview-09-2025": [ 3e-7, 0.0000025, 7.5e-8, 0, 0 ], + "gemini-2.5-pro": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gemini-2.5-pro-preview-tts": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gemini-3-flash-preview": [ 5e-7, 0.000003, 5e-8, 0, 0 ], + "gemini-3-pro-preview": [ 0.000002, 0.000012, 2e-7, 0, 0 ], + "gemini-3.1-flash-lite": [ 2.5e-7, 0.0000015, 2.5e-8, 0, 0 ], + "gemini-3.1-flash-lite-preview": [ 2.5e-7, 0.0000015, 2.5e-8, 0, 0 ], + "gemini-3.1-flash-live-preview": [ 7.5e-7, 0.0000045, 7.5e-7, 0, 0 ], + "gemini-3.1-pro-preview": [ 0.000002, 0.000012, 2e-7, 0, 0 ], + "gemini-3.1-pro-preview-customtools": [ 0.000002, 0.000012, 2e-7, 0, 0 ], + "gemini-3.5-flash": [ 0.0000015, 0.000009, 1.5e-7, 0, 0 ], + "gemini-3.5-flash-lite": [ 3e-7, 0.0000025, 3e-8, 0, 0 ], + "gemini-3.6-flash": [ 0.0000015, 0.0000075, 1.5e-7, 0, 0 ], + "gemini-exp-1114": [ 0, 0, 0, 0, 0 ], + "gemini-exp-1206": [ 3e-7, 0.0000025, 3e-8, 0, 0 ], + "gemini-flash-latest": [ 3e-7, 0.0000025, 3e-8, 0, 0 ], + "gemini-flash-lite-latest": [ 1e-7, 4e-7, 1e-8, 0, 0 ], + "gemini-gemma-2-27b-it": [ 3.5e-7, 0.00000105, 3.5e-7, 0, 0 ], + "gemini-gemma-2-9b-it": [ 3.5e-7, 0.00000105, 3.5e-7, 0, 0 ], + "gemini-omni-flash-preview": [ 0.0000015, 0.000009, 0.0000015, 0, 0 ], + "gemini-pro-latest": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gemini-robotics-er-1.5-preview": [ 3e-7, 0.0000025, 0, 0, 0 ], + "gemma-3-27b-it": [ 0, 0, 0, 0, 0 ], + "gpt-3.5-turbo": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "gpt-3.5-turbo-0125": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "gpt-3.5-turbo-1106": [ 0.000001, 0.000002, 0.000001, 0, 0 ], + "gpt-3.5-turbo-16k": [ 0.000003, 0.000004, 0.000003, 0, 0 ], + "gpt-4": [ 0.00003, 0.00006, 0.00003, 0, 0 ], + "gpt-4-0125-preview": [ 0.00001, 0.00003, 0.00001, 0, 0 ], + "gpt-4-0314": [ 0.00003, 0.00006, 0.00003, 0, 0 ], + "gpt-4-0613": [ 0.00003, 0.00006, 0.00003, 0, 0 ], + "gpt-4-1106-preview": [ 0.00001, 0.00003, 0.00001, 0, 0 ], + "gpt-4-turbo": [ 0.00001, 0.00003, 0.00001, 0, 0 ], + "gpt-4-turbo-2024-04-09": [ 0.00001, 0.00003, 0.00001, 0, 0 ], + "gpt-4-turbo-preview": [ 0.00001, 0.00003, 0.00001, 0, 0 ], + "gpt-4.1": [ 0.000002, 0.000008, 5e-7, 0, 0 ], + "gpt-4.1-2025-04-14": [ 0.000002, 0.000008, 5e-7, 0, 0 ], + "gpt-4.1-mini": [ 4e-7, 0.0000016, 1e-7, 0, 0 ], + "gpt-4.1-mini-2025-04-14": [ 4e-7, 0.0000016, 1e-7, 0, 0 ], + "gpt-4.1-nano": [ 1e-7, 4e-7, 2.5e-8, 0, 0 ], + "gpt-4.1-nano-2025-04-14": [ 1e-7, 4e-7, 2.5e-8, 0, 0 ], + "gpt-4o": [ 0.0000025, 0.00001, 0.00000125, 0, 0 ], + "gpt-4o-2024-05-13": [ 0.000005, 0.000015, 0.000005, 0, 0 ], + "gpt-4o-2024-08-06": [ 0.0000025, 0.00001, 0.00000125, 0, 0 ], + "gpt-4o-2024-11-20": [ 0.0000025, 0.00001, 0.00000125, 0, 0 ], + "gpt-4o-audio-preview": [ 0.0000025, 0.00001, 0.0000025, 0, 0 ], + "gpt-4o-audio-preview-2024-12-17": [ 0.0000025, 0.00001, 0.0000025, 0, 0 ], + "gpt-4o-audio-preview-2025-06-03": [ 0.0000025, 0.00001, 0.0000025, 0, 0 ], + "gpt-4o-mini": [ 1.5e-7, 6e-7, 7.5e-8, 0, 0 ], + "gpt-4o-mini-2024-07-18": [ 1.5e-7, 6e-7, 7.5e-8, 0, 0 ], + "gpt-4o-mini-audio-preview": [ 1.5e-7, 6e-7, 1.5e-7, 0, 0 ], + "gpt-4o-mini-audio-preview-2024-12-17": [ 1.5e-7, 6e-7, 1.5e-7, 0, 0 ], + "gpt-4o-mini-search-preview": [ 1.5e-7, 6e-7, 7.5e-8, 0, 0 ], + "gpt-4o-mini-search-preview-2025-03-11": [ 1.5e-7, 6e-7, 7.5e-8, 0, 0 ], + "gpt-4o-search-preview": [ 0.0000025, 0.00001, 0.00000125, 0, 0 ], + "gpt-4o-search-preview-2025-03-11": [ 0.0000025, 0.00001, 0.00000125, 0, 0 ], + "gpt-5": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5-2025-08-07": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5-chat": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5-chat-latest": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5-codex": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5-mini": [ 2.5e-7, 0.000002, 2.5e-8, 0, 0 ], + "gpt-5-mini-2025-08-07": [ 2.5e-7, 0.000002, 2.5e-8, 0, 0 ], + "gpt-5-nano": [ 5e-8, 4e-7, 5e-9, 0, 0 ], + "gpt-5-nano-2025-08-07": [ 5e-8, 4e-7, 5e-9, 0, 0 ], + "gpt-5-pro": [ 0.000015, 0.00012, 0.000015, 0, 0 ], + "gpt-5-pro-2025-10-06": [ 0.000015, 0.00012, 0.000015, 0, 0 ], + "gpt-5-search-api": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5-search-api-2025-10-14": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5.1": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5.1-2025-11-13": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5.1-chat-latest": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5.1-codex": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5.1-codex-max": [ 0.00000125, 0.00001, 1.25e-7, 0, 0 ], + "gpt-5.1-codex-mini": [ 2.5e-7, 0.000002, 2.5e-8, 0, 0 ], + "gpt-5.2": [ 0.00000175, 0.000014, 1.75e-7, 0, 0 ], + "gpt-5.2-2025-12-11": [ 0.00000175, 0.000014, 1.75e-7, 0, 0 ], + "gpt-5.2-chat-latest": [ 0.00000175, 0.000014, 1.75e-7, 0, 0 ], + "gpt-5.2-codex": [ 0.00000175, 0.000014, 1.75e-7, 0, 0 ], + "gpt-5.2-pro": [ 0.000021, 0.000168, 0.000021, 0, 0 ], + "gpt-5.2-pro-2025-12-11": [ 0.000021, 0.000168, 0.000021, 0, 0 ], + "gpt-5.3-chat-latest": [ 0.00000175, 0.000014, 1.75e-7, 0, 0 ], + "gpt-5.3-codex": [ 0.00000175, 0.000014, 1.75e-7, 0, 0 ], + "gpt-5.4": [ 0.0000025, 0.000015, 2.5e-7, 0, 0 ], + "gpt-5.4-2026-03-05": [ 0.0000025, 0.000015, 2.5e-7, 0, 0 ], + "gpt-5.4-mini": [ 7.5e-7, 0.0000045, 7.5e-8, 0, 0 ], + "gpt-5.4-mini-2026-03-17": [ 7.5e-7, 0.0000045, 7.5e-8, 0, 0 ], + "gpt-5.4-nano": [ 2e-7, 0.00000125, 2e-8, 0, 0 ], + "gpt-5.4-nano-2026-03-17": [ 2e-7, 0.00000125, 2e-8, 0, 0 ], + "gpt-5.4-pro": [ 0.00003, 0.00018, 0.000003, 0, 0 ], + "gpt-5.4-pro-2026-03-05": [ 0.00003, 0.00018, 0.000003, 0, 0 ], + "gpt-5.5": [ 0.000005, 0.00003, 5e-7, 0, 0 ], + "gpt-5.5-2026-04-23": [ 0.000005, 0.00003, 5e-7, 0, 0 ], + "gpt-5.5-pro": [ 0.00003, 0.00018, 0.000003, 0, 0 ], + "gpt-5.5-pro-2026-04-23": [ 0.00003, 0.00018, 0.000003, 0, 0 ], + "gpt-5.6": [ 0.000005, 0.00003, 5e-7, 0.00000625, 0.00000625 ], + "gpt-5.6-luna": [ 2e-7, 0.0000012, 2e-8, 2.5e-7, 2.5e-7 ], + "gpt-5.6-sol": [ 0.000005, 0.00003, 5e-7, 0.00000625, 0.00000625 ], + "gpt-5.6-terra": [ 0.000002, 0.000012, 2e-7, 0.0000025, 0.0000025 ], + "gpt-audio": [ 0.0000025, 0.00001, 0.0000025, 0, 0 ], + "gpt-audio-1.5": [ 0.0000025, 0.00001, 0.0000025, 0, 0 ], + "gpt-audio-2025-08-28": [ 0.0000025, 0.00001, 0.0000025, 0, 0 ], + "gpt-audio-mini": [ 6e-7, 0.0000024, 6e-7, 0, 0 ], + "gpt-audio-mini-2025-10-06": [ 6e-7, 0.0000024, 6e-7, 0, 0 ], + "gpt-audio-mini-2025-12-15": [ 6e-7, 0.0000024, 6e-7, 0, 0 ], + "grok-2": [ 0.000002, 0.00001, 0.000002, 0, 0 ], + "grok-2-1212": [ 0.000002, 0.00001, 0.000002, 0, 0 ], + "grok-2-latest": [ 0.000002, 0.00001, 0.000002, 0, 0 ], + "grok-2-vision": [ 0.000002, 0.00001, 0.000002, 0, 0 ], + "grok-2-vision-1212": [ 0.000002, 0.00001, 0.000002, 0, 0 ], + "grok-2-vision-latest": [ 0.000002, 0.00001, 0.000002, 0, 0 ], + "grok-3": [ 0.000003, 0.000015, 7.5e-7, 0, 0 ], + "grok-3-beta": [ 0.000003, 0.000015, 7.5e-7, 0, 0 ], + "grok-3-fast-beta": [ 0.000005, 0.000025, 0.00000125, 0, 0 ], + "grok-3-fast-latest": [ 0.000005, 0.000025, 0.00000125, 0, 0 ], + "grok-3-latest": [ 0.000003, 0.000015, 7.5e-7, 0, 0 ], + "grok-3-mini": [ 3e-7, 5e-7, 7.5e-8, 0, 0 ], + "grok-3-mini-beta": [ 3e-7, 5e-7, 7.5e-8, 0, 0 ], + "grok-3-mini-fast": [ 6e-7, 0.000004, 1.5e-7, 0, 0 ], + "grok-3-mini-fast-beta": [ 6e-7, 0.000004, 1.5e-7, 0, 0 ], + "grok-3-mini-fast-latest": [ 6e-7, 0.000004, 1.5e-7, 0, 0 ], + "grok-3-mini-latest": [ 3e-7, 5e-7, 7.5e-8, 0, 0 ], + "grok-4": [ 0.000003, 0.000015, 0.000003, 0, 0 ], + "grok-4-0709": [ 0.000003, 0.000015, 0.000003, 0, 0 ], + "grok-4-1-fast": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-1-fast-non-reasoning": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-1-fast-non-reasoning-latest": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-1-fast-reasoning": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-1-fast-reasoning-latest": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-fast-non-reasoning": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-fast-reasoning": [ 2e-7, 5e-7, 5e-8, 0, 0 ], + "grok-4-latest": [ 0.000003, 0.000015, 0.000003, 0, 0 ], + "grok-4.20-0309-reasoning": [ 0.000002, 0.000006, 2e-7, 0, 0 ], + "grok-4.20-beta-0309-non-reasoning": [ 0.000002, 0.000006, 2e-7, 0, 0 ], + "grok-4.20-beta-0309-reasoning": [ 0.000002, 0.000006, 2e-7, 0, 0 ], + "grok-4.20-multi-agent-beta-0309": [ 0.000002, 0.000006, 2e-7, 0, 0 ], + "grok-4.3": [ 0.00000125, 0.0000025, 2e-7, 0, 0 ], + "grok-4.3-latest": [ 0.00000125, 0.0000025, 2e-7, 0, 0 ], + "grok-4.5": [ 0.000002, 0.000006, 5e-7, 0, 0 ], + "grok-4.5-latest": [ 0.000002, 0.000006, 5e-7, 0, 0 ], + "grok-beta": [ 0.000005, 0.000015, 0.000005, 0, 0 ], + "grok-code-fast": [ 2e-7, 0.0000015, 2e-8, 0, 0 ], + "grok-code-fast-1": [ 2e-7, 0.0000015, 2e-8, 0, 0 ], + "grok-code-fast-1-0825": [ 2e-7, 0.0000015, 2e-8, 0, 0 ], + "grok-vision-beta": [ 0.000005, 0.000015, 0.000005, 0, 0 ], + "labs-devstral-small-2512": [ 1e-7, 3e-7, 1e-7, 0, 0 ], + "learnlm-1.5-pro-experimental": [ 0, 0, 0, 0, 0 ], + "lyria-3-clip-preview": [ 0, 0, 0, 0, 0 ], + "lyria-3-pro-preview": [ 0, 0, 0, 0, 0 ], + "magistral-medium-1-2-2509": [ 0.000002, 0.000005, 0.000002, 0, 0 ], + "magistral-medium-2506": [ 0.000002, 0.000005, 0.000002, 0, 0 ], + "magistral-medium-2509": [ 0.000002, 0.000005, 0.000002, 0, 0 ], + "magistral-medium-latest": [ 0.000002, 0.000005, 0.000002, 0, 0 ], + "magistral-small-1-2-2509": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "magistral-small-2506": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "magistral-small-latest": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "ministral-3-14b-2512": [ 2e-7, 2e-7, 2e-7, 0, 0 ], + "ministral-3-3b-2512": [ 1e-7, 1e-7, 1e-7, 0, 0 ], + "ministral-3-8b-2512": [ 1.5e-7, 1.5e-7, 1.5e-7, 0, 0 ], + "ministral-8b-2512": [ 1.5e-7, 1.5e-7, 1.5e-7, 0, 0 ], + "ministral-8b-latest": [ 1.5e-7, 1.5e-7, 1.5e-7, 0, 0 ], + "mistral-large-2402": [ 0.000004, 0.000012, 0.000004, 0, 0 ], + "mistral-large-2407": [ 0.000003, 0.000009, 0.000003, 0, 0 ], + "mistral-large-2411": [ 0.000002, 0.000006, 0.000002, 0, 0 ], + "mistral-large-2512": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "mistral-large-3": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "mistral-large-latest": [ 5e-7, 0.0000015, 5e-7, 0, 0 ], + "mistral-medium": [ 0.0000027, 0.0000081, 0.0000027, 0, 0 ], + "mistral-medium-2312": [ 0.0000027, 0.0000081, 0.0000027, 0, 0 ], + "mistral-medium-2505": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "mistral-medium-2508": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "mistral-medium-2604": [ 0.0000015, 0.0000075, 0.0000015, 0, 0 ], + "mistral-medium-3-1-2508": [ 4e-7, 0.000002, 4e-7, 0, 0 ], + "mistral-medium-3-5": [ 0.0000015, 0.0000075, 0.0000015, 0, 0 ], + "mistral-medium-latest": [ 0.0000015, 0.0000075, 0.0000015, 0, 0 ], + "mistral-small": [ 1e-7, 3e-7, 1e-7, 0, 0 ], + "mistral-small-3-2-2506": [ 6e-8, 1.8e-7, 6e-8, 0, 0 ], + "mistral-small-latest": [ 6e-8, 1.8e-7, 6e-8, 0, 0 ], + "mistral-tiny": [ 2.5e-7, 2.5e-7, 2.5e-7, 0, 0 ], + "o1": [ 0.000015, 0.00006, 0.0000075, 0, 0 ], + "o1-2024-12-17": [ 0.000015, 0.00006, 0.0000075, 0, 0 ], + "o1-pro": [ 0.00015, 0.0006, 0.00015, 0, 0 ], + "o1-pro-2025-03-19": [ 0.00015, 0.0006, 0.00015, 0, 0 ], + "o3": [ 0.000002, 0.000008, 5e-7, 0, 0 ], + "o3-2025-04-16": [ 0.000002, 0.000008, 5e-7, 0, 0 ], + "o3-deep-research": [ 0.00001, 0.00004, 0.0000025, 0, 0 ], + "o3-deep-research-2025-06-26": [ 0.00001, 0.00004, 0.0000025, 0, 0 ], + "o3-mini": [ 0.0000011, 0.0000044, 5.5e-7, 0, 0 ], + "o3-mini-2025-01-31": [ 0.0000011, 0.0000044, 5.5e-7, 0, 0 ], + "o3-pro": [ 0.00002, 0.00008, 0.00002, 0, 0 ], + "o3-pro-2025-06-10": [ 0.00002, 0.00008, 0.00002, 0, 0 ], + "o4-mini": [ 0.0000011, 0.0000044, 2.75e-7, 0, 0 ], + "o4-mini-2025-04-16": [ 0.0000011, 0.0000044, 2.75e-7, 0, 0 ], + "o4-mini-deep-research": [ 0.000002, 0.000008, 5e-7, 0, 0 ], + "o4-mini-deep-research-2025-06-26": [ 0.000002, 0.000008, 5e-7, 0, 0 ], + "open-codestral-mamba": [ 2.5e-7, 2.5e-7, 2.5e-7, 0, 0 ], + "open-mistral-7b": [ 2.5e-7, 2.5e-7, 2.5e-7, 0, 0 ], + "open-mistral-nemo": [ 3e-7, 3e-7, 3e-7, 0, 0 ], + "open-mistral-nemo-2407": [ 3e-7, 3e-7, 3e-7, 0, 0 ], + "open-mixtral-8x22b": [ 0.000002, 0.000006, 0.000002, 0, 0 ], + "open-mixtral-8x7b": [ 7e-7, 7e-7, 7e-7, 0, 0 ], + "pixtral-12b-2409": [ 1.5e-7, 1.5e-7, 1.5e-7, 0, 0 ], + "pixtral-large-2411": [ 0.000002, 0.000006, 0.000002, 0, 0 ], + "pixtral-large-latest": [ 0.000002, 0.000006, 0.000002, 0, 0 ] +} diff --git a/tool/src/ase-statusline.ts b/tool/src/ase-statusline.ts index d6baae44..101b8a12 100644 --- a/tool/src/ase-statusline.ts +++ b/tool/src/ase-statusline.ts @@ -17,6 +17,7 @@ import type { ForegroundColorName } from "chalk" import type Log from "./ase-log.js" import { Config, configSchema, parseScope } from "./ase-config.js" import { readStdin, writeStdout } from "./ase-stdio.js" +import { monthCostForRender, refreshMonthCostCache } from "./ase-statusline-cost.js" import pkg from "../package.json" with { type: "json" } /* forced-color chalk instance: stdout is a pipe under Anthropic Claude Code CLI, @@ -101,12 +102,14 @@ interface StatuslineInput { /* internal command options type */ interface StatuslineOpts { - tool: string - width: number - margin: number - padding: number - icons: boolean - labels: boolean + tool: string + width: number + margin: number + padding: number + icons: boolean + labels: boolean + monthCostTtl: number + refreshMonthCost: boolean } /* custom argument parser for Commander: non-negative integer */ @@ -320,12 +323,25 @@ export default class StatuslineCommand { "disable icons in placeholder rendering") .option("--no-labels", "disable labels in front of bold values") + .option("--month-cost-ttl ", + "seconds to cache the %Y current-month total cost before a background refresh", + parseInteger("--month-cost-ttl"), 300) + .option("--refresh-month-cost", + "(internal) recompute and cache the current-month total cost, then exit") .argument("[lines...]", "one or more template lines with %u %p %T %s %m %e %t %O %P %h %c %C %a %r " + - "%S %D %W %Q %H %X %b %g %G %d %M %V placeholders and ... markup " + + "%S %D %W %Q %H %X %Y %b %g %G %d %M %V placeholders and ... markup " + "(color: black, red, green, yellow, blue, magenta, cyan, white, default) " + "(default: single line \"%m %e %t\")") .action(async (lines: string[], opts: StatuslineOpts) => { + /* internal mode: recompute the current-month cost cache and + exit, as invoked by the detached background refresh that + %Y spawns during a normal render (no stdin, no rendering) */ + if (opts.refreshMonthCost) { + refreshMonthCostCache(new Date()) + return + } + /* validate target tool */ const tool = this.parseTool(opts.tool) @@ -547,6 +563,11 @@ export default class StatuslineCommand { if (sessCost !== undefined) emit(`${prefix("$", "cost")}${c.bold(formatCostUsd(sessCost))}`) }, + Y: () => { + const monthCost = monthCostForRender(new Date(), opts.monthCostTtl) + if (monthCost !== null) + emit(`${prefix("∑", "month")}${c.bold(formatCostUsd(monthCost))}`) + }, /* ==== VERSION CONTROL ==== */ a: () => {