Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
33 changes: 30 additions & 3 deletions docs/usage-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -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}`.
Expand Down
94 changes: 94 additions & 0 deletions tool/etc/litellm-prices.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
** Agentic Software Engineering (ASE)
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
** Licensed under Apache 2.0 <https://spdx.org/licenses/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 <rse@engelschall.com>
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
*/

/* 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<Record<string, Price>> = {
${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()
4 changes: 4 additions & 0 deletions tool/etc/stx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading