Skip to content

Commit ac31883

Browse files
committed
Merge PR #59: OMP provider and model alias mapping
Adds Oh My Pi support by parameterizing the Pi JSONL reader to accept a providerName, so sessions at ~/.omp/agent/sessions/ are discovered and tracked alongside Pi. Ships a model-alias CLI command plus five built-in aliases for the anthropic--claude-X.Y-tier double-dash format that some Anthropic-compatible proxies emit, so cost rows no longer read $0.00 for those names. Contributed by @cgrossde.
2 parents d69aa34 + 4f11382 commit ac31883

9 files changed

Lines changed: 505 additions & 20 deletions

File tree

README.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/dashboard.jpg" alt="CodeBurn TUI dashboard" width="620" />
1818
</p>
1919

20-
By task type, tool, model, MCP server, and project. Supports **Claude Code**, **Codex** (OpenAI), **Cursor**, **cursor-agent**, **OpenCode**, **Pi**, and **GitHub Copilot** with a provider plugin system. Tracks one-shot success rate per activity type so you can see where the AI nails it first try vs. burns tokens on edit/test/fix retries. Interactive TUI dashboard with gradient charts, responsive panels, and keyboard navigation. Native macOS menubar app in `mac/`. CSV/JSON export.
20+
By task type, tool, model, MCP server, and project. Supports **Claude Code**, **Codex** (OpenAI), **Cursor**, **cursor-agent**, **OpenCode**, **Pi**, **[OMP](https://github.com/can1357/oh-my-pi)** (Oh My Pi), and **GitHub Copilot** with a provider plugin system. Tracks one-shot success rate per activity type so you can see where the AI nails it first try vs. burns tokens on edit/test/fix retries. Interactive TUI dashboard with gradient charts, responsive panels, and keyboard navigation. Native macOS menubar app in `mac/`. CSV/JSON export.
2121

2222
Works by reading session data directly from disk. No wrapper, no proxy, no API keys. Pricing from LiteLLM (auto-cached, all models supported).
2323

@@ -36,7 +36,7 @@ npx codeburn
3636
### Requirements
3737

3838
- Node.js 20+
39-
- Claude Code (`~/.claude/projects/`), Codex (`~/.codex/sessions/`), Cursor, OpenCode, Pi (`~/.pi/agent/sessions/`), and/or GitHub Copilot (`~/.copilot/session-state/`)
39+
- Claude Code (`~/.claude/projects/`), Codex (`~/.codex/sessions/`), Cursor, OpenCode, Pi (`~/.pi/agent/sessions/`), OMP (`~/.omp/agent/sessions/`), and/or GitHub Copilot (`~/.copilot/session-state/`)
4040
- For Cursor/OpenCode support: `better-sqlite3` is installed automatically as an optional dependency
4141

4242
## Usage
@@ -93,6 +93,7 @@ codeburn report --provider cursor-agent # cursor-agent CLI only
9393
codeburn report --provider opencode # OpenCode only
9494
codeburn report --provider pi # Pi only
9595
codeburn report --provider copilot # GitHub Copilot only
96+
codeburn report --provider omp # OMP only
9697
codeburn today --provider codex # Codex today
9798
codeburn export --provider claude # export Claude data only
9899
```
@@ -136,6 +137,7 @@ Either flag alone is valid. Inverted or malformed dates exit with a clear error.
136137
| Cursor | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` | Supported |
137138
| OpenCode | `~/.local/share/opencode/` (SQLite) | Supported |
138139
| Pi | `~/.pi/agent/sessions/` | Supported |
140+
| OMP | `~/.omp/agent/sessions/` | Supported |
139141
| GitHub Copilot | `~/.copilot/session-state/` | Supported (output tokens only) |
140142
| Amp | -- | Planned (provider plugin system) |
141143

@@ -149,6 +151,22 @@ GitHub Copilot only logs output tokens in its session state, so Copilot cost row
149151

150152
The provider plugin system makes adding a new provider a single file. Each provider implements session discovery, JSONL parsing, tool normalization, and model display names. See `src/providers/codex.ts` for an example.
151153

154+
## Model aliases
155+
156+
If you see `$0.00` for some models, the model name reported by your provider doesn't match any entry in the LiteLLM pricing data. This commonly happens when using a proxy that rewrites model names.
157+
158+
Map any model name to a canonical one:
159+
160+
```bash
161+
codeburn model-alias "my-proxy-model" "claude-opus-4-6" # add alias
162+
codeburn model-alias --list # show configured aliases
163+
codeburn model-alias --remove "my-proxy-model" # remove alias
164+
```
165+
166+
Aliases are stored in `~/.config/codeburn/config.json` and applied at runtime before pricing lookup. The target name can be anything in the [LiteLLM model list](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) or a canonical name from the fallback table (e.g. `claude-sonnet-4-6`, `claude-opus-4-5`, `gpt-4o`).
167+
168+
Built-in aliases ship for known proxy model name variants (such as `anthropic--claude-4.6-opus`). User-configured aliases take precedence over built-ins.
169+
152170
## Currency
153171

154172
By default, costs are shown in USD. To display in a different currency:
@@ -306,9 +324,9 @@ All metrics are computed from your local session data. No LLM calls, fully deter
306324

307325
**OpenCode** stores sessions in SQLite databases at `~/.local/share/opencode/opencode*.db`. CodeBurn queries the `session`, `message`, and `part` tables read-only, extracts token counts and tool usage, and recalculates cost using the LiteLLM pricing engine. Falls back to OpenCode's own cost field for models not in our pricing data. Subtask sessions (`parent_id IS NOT NULL`) are excluded to avoid double-counting. Supports multiple channel databases and respects `XDG_DATA_HOME`.
308326

309-
**Pi** stores sessions as JSONL at `~/.pi/agent/sessions/<sanitized-cwd>/*.jsonl`. Each assistant message carries token usage (input, output, cacheRead, cacheWrite) plus inline `toolCall` content blocks. CodeBurn extracts token counts, normalizes Pi's lowercase tool names to the standard set (`bash` -> `Bash`, `dispatch_agent` -> `Agent`), and pulls bash commands from `toolCall.arguments.command` for the shell breakdown.
327+
**Pi / OMP** stores sessions as JSONL at `~/.pi/agent/sessions/<sanitized-cwd>/*.jsonl` (Pi) and `~/.omp/agent/sessions/<sanitized-cwd>/*.jsonl` (OMP). Each assistant message carries token usage (input, output, cacheRead, cacheWrite) plus inline `toolCall` content blocks. CodeBurn extracts token counts, normalizes tool names to the standard set (`bash` -> `Bash`, `dispatch_agent` -> `Agent`), and pulls bash commands from `toolCall.arguments.command` for the shell breakdown.
310328

311-
CodeBurn reads these files, deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session+message ID for OpenCode, by responseId for Pi), filters by date range per entry, and classifies each turn.
329+
CodeBurn reads these files, deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session+message ID for OpenCode, by responseId for Pi/OMP), filters by date range per entry, and classifies each turn.
312330

313331
## Environment variables
314332

@@ -342,7 +360,7 @@ src/
342360
codex.ts Codex session discovery and JSONL parsing
343361
cursor.ts Cursor SQLite parsing, language extraction
344362
opencode.ts OpenCode SQLite session discovery and parsing
345-
pi.ts Pi agent JSONL session discovery and parsing
363+
pi.ts Pi/OMP agent JSONL session discovery and parsing
346364
```
347365

348366
## Star History

src/cli.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from 'commander'
22
import { installMenubarApp } from './menubar-installer.js'
33
import { exportCsv, exportJson, type PeriodExport } from './export.js'
4-
import { loadPricing } from './models.js'
4+
import { loadPricing, setModelAliases } from './models.js'
55
import { parseAllSessions, filterProjectsByName } from './parser.js'
66
import { convertCost } from './currency.js'
77
import { renderStatusBar } from './format.js'
@@ -139,6 +139,8 @@ const program = new Command()
139139
.option('--verbose', 'print warnings to stderr on read failures and skipped files')
140140

141141
program.hook('preAction', async (thisCommand) => {
142+
const config = await readConfig()
143+
setModelAliases(config.modelAliases ?? {})
142144
if (thisCommand.opts<{ verbose?: boolean }>().verbose) {
143145
process.env['CODEBURN_VERBOSE'] = '1'
144146
}
@@ -689,6 +691,56 @@ program
689691
console.log(` Config saved to ${getConfigFilePath()}\n`)
690692
})
691693

694+
program
695+
.command('model-alias [from] [to]')
696+
.description('Map a provider model name to a canonical one for pricing (e.g. codeburn model-alias my-model claude-opus-4-6)')
697+
.option('--remove <from>', 'Remove an alias')
698+
.option('--list', 'List configured aliases')
699+
.action(async (from?: string, to?: string, opts?: { remove?: string; list?: boolean }) => {
700+
const config = await readConfig()
701+
const aliases = config.modelAliases ?? {}
702+
703+
if (opts?.list || (!from && !opts?.remove)) {
704+
const entries = Object.entries(aliases)
705+
if (entries.length === 0) {
706+
console.log('\n No model aliases configured.')
707+
console.log(` Config: ${getConfigFilePath()}\n`)
708+
} else {
709+
console.log('\n Model aliases:')
710+
for (const [src, dst] of entries) {
711+
console.log(` ${src} -> ${dst}`)
712+
}
713+
console.log(` Config: ${getConfigFilePath()}\n`)
714+
}
715+
return
716+
}
717+
718+
if (opts?.remove) {
719+
if (!(opts.remove in aliases)) {
720+
console.error(`\n Alias not found: ${opts.remove}\n`)
721+
process.exitCode = 1
722+
return
723+
}
724+
delete aliases[opts.remove]
725+
config.modelAliases = Object.keys(aliases).length > 0 ? aliases : undefined
726+
await saveConfig(config)
727+
console.log(`\n Removed alias: ${opts.remove}\n`)
728+
return
729+
}
730+
731+
if (!from || !to) {
732+
console.error('\n Usage: codeburn model-alias <from> <to>\n')
733+
process.exitCode = 1
734+
return
735+
}
736+
737+
aliases[from] = to
738+
config.modelAliases = aliases
739+
await saveConfig(config)
740+
console.log(`\n Alias saved: ${from} -> ${to}`)
741+
console.log(` Config: ${getConfigFilePath()}\n`)
742+
})
743+
692744
program
693745
.command('plan [action] [id]')
694746
.description('Show or configure a subscription plan for overage tracking')

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type CodeburnConfig = {
1919
symbol?: string
2020
}
2121
plan?: Plan
22+
modelAliases?: Record<string, string>
2223
}
2324

2425
function getConfigDir(): string {

src/models.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,40 @@ export async function loadPricing(): Promise<void> {
126126
}
127127
}
128128

129+
// Known model name variants that providers emit but LiteLLM/fallback don't index under.
130+
// OMP emits 'anthropic--claude-4.6-opus' (double-dash, dot version, tier-last).
131+
// getCanonicalName strips any 'provider/' prefix first, so only the post-strip
132+
// forms need to be listed here.
133+
const BUILTIN_ALIASES: Record<string, string> = {
134+
'anthropic--claude-4.6-opus': 'claude-opus-4-6',
135+
'anthropic--claude-4.6-sonnet': 'claude-sonnet-4-6',
136+
'anthropic--claude-4.5-opus': 'claude-opus-4-5',
137+
'anthropic--claude-4.5-sonnet': 'claude-sonnet-4-5',
138+
'anthropic--claude-4.5-haiku': 'claude-haiku-4-5',
139+
}
140+
141+
let userAliases: Record<string, string> = {}
142+
143+
// Called once during CLI startup after config is loaded.
144+
// User aliases take precedence over built-ins.
145+
export function setModelAliases(aliases: Record<string, string>): void {
146+
userAliases = aliases
147+
}
148+
149+
function resolveAlias(model: string): string {
150+
if (Object.hasOwn(userAliases, model)) return userAliases[model]!
151+
if (Object.hasOwn(BUILTIN_ALIASES, model)) return BUILTIN_ALIASES[model]!
152+
return model
153+
}
129154
function getCanonicalName(model: string): string {
130155
return model
131-
.replace(/@.*$/, '')
132-
.replace(/-\d{8}$/, '')
156+
.replace(/@.*$/, '') // strip pin: claude-sonnet-4-6@20250929 -> claude-sonnet-4-6
157+
.replace(/-\d{8}$/, '') // strip date: claude-sonnet-4-20250514 -> claude-sonnet-4
158+
.replace(/^[^/]+\//, '') // strip provider prefix: anthropic/foo -> foo
133159
}
134160

135161
export function getModelCosts(model: string): ModelCosts | null {
136-
const canonical = getCanonicalName(model)
162+
const canonical = resolveAlias(getCanonicalName(model))
137163

138164
if (pricingCache?.has(canonical)) return pricingCache.get(canonical)!
139165

@@ -176,7 +202,7 @@ export function calculateCost(
176202
}
177203

178204
export function getShortModelName(model: string): string {
179-
const canonical = getCanonicalName(model)
205+
const canonical = resolveAlias(getCanonicalName(model))
180206
const shortNames: Record<string, string> = {
181207
'claude-opus-4-7': 'Opus 4.7',
182208
'claude-opus-4-6': 'Opus 4.6',

src/providers/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { claude } from './claude.js'
22
import { codex } from './codex.js'
33
import { copilot } from './copilot.js'
4-
import { pi } from './pi.js'
4+
import { pi, omp } from './pi.js'
55
import type { Provider, SessionSource } from './types.js'
66

77
let cursorProvider: Provider | null = null
@@ -49,7 +49,7 @@ async function loadCursorAgent(): Promise<Provider | null> {
4949
}
5050
}
5151

52-
const coreProviders: Provider[] = [claude, codex, copilot, pi]
52+
const coreProviders: Provider[] = [claude, codex, copilot, pi, omp]
5353

5454
export async function getAllProviders(): Promise<Provider[]> {
5555
const [cursor, opencode, cursorAgent] = await Promise.all([loadCursor(), loadOpenCode(), loadCursorAgent()])

src/providers/pi.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ function getPiSessionsDir(override?: string): string {
5656
return override ?? join(homedir(), '.pi', 'agent', 'sessions')
5757
}
5858

59+
function getOmpSessionsDir(override?: string): string {
60+
return override ?? join(homedir(), '.omp', 'agent', 'sessions')
61+
}
62+
5963
async function readFirstEntry(filePath: string): Promise<PiEntry | null> {
6064
const content = await readSessionFile(filePath)
6165
if (content === null) return null
@@ -68,7 +72,7 @@ async function readFirstEntry(filePath: string): Promise<PiEntry | null> {
6872
}
6973
}
7074

71-
async function discoverSessionsInDir(sessionsDir: string): Promise<SessionSource[]> {
75+
async function discoverSessionsInDir(sessionsDir: string, providerName: string): Promise<SessionSource[]> {
7276
const sources: SessionSource[] = []
7377

7478
let projectDirs: string[]
@@ -100,7 +104,7 @@ async function discoverSessionsInDir(sessionsDir: string): Promise<SessionSource
100104
if (!first || first.type !== 'session') continue
101105

102106
const cwd = first.cwd ?? dirName
103-
sources.push({ path: filePath, project: basename(cwd), provider: 'pi' })
107+
sources.push({ path: filePath, project: basename(cwd), provider: providerName })
104108
}
105109
}
106110

@@ -150,7 +154,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
150154

151155
const model = msg.model ?? 'gpt-5'
152156
const responseId = msg.responseId ?? ''
153-
const dedupKey = `pi:${source.path}:${responseId || entry.id || entry.timestamp || String(lineIdx)}`
157+
const dedupKey = `${source.provider}:${source.path}:${responseId || entry.id || entry.timestamp || String(lineIdx)}`
154158

155159
if (seenKeys.has(dedupKey)) continue
156160
seenKeys.add(dedupKey)
@@ -168,7 +172,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
168172
const timestamp = entry.timestamp ?? ''
169173

170174
yield {
171-
provider: 'pi',
175+
provider: source.provider,
172176
model,
173177
inputTokens: input,
174178
outputTokens: output,
@@ -212,7 +216,7 @@ export function createPiProvider(sessionsDir?: string): Provider {
212216
},
213217

214218
async discoverSessions(): Promise<SessionSource[]> {
215-
return discoverSessionsInDir(dir)
219+
return discoverSessionsInDir(dir, 'pi')
216220
},
217221

218222
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
@@ -222,3 +226,33 @@ export function createPiProvider(sessionsDir?: string): Provider {
222226
}
223227

224228
export const pi = createPiProvider()
229+
230+
export function createOmpProvider(sessionsDir?: string): Provider {
231+
const dir = getOmpSessionsDir(sessionsDir)
232+
233+
return {
234+
name: 'omp',
235+
displayName: 'OMP',
236+
237+
modelDisplayName(model: string): string {
238+
for (const [key, name] of modelDisplayEntries) {
239+
if (model.startsWith(key)) return name
240+
}
241+
return model
242+
},
243+
244+
toolDisplayName(rawTool: string): string {
245+
return toolNameMap[rawTool] ?? rawTool
246+
},
247+
248+
async discoverSessions(): Promise<SessionSource[]> {
249+
return discoverSessionsInDir(dir, 'omp')
250+
},
251+
252+
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
253+
return createParser(source, seenKeys)
254+
},
255+
}
256+
}
257+
258+
export const omp = createOmpProvider()

0 commit comments

Comments
 (0)