Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@
reconcile. Closes #279.

### Fixed (CLI)
- **Cursor cost shown for every model, not just Auto.** Cursor emits model
names in a `claude-<dot-version>-<tier>` shape (`claude-4.6-sonnet`,
`claude-4.5-opus`, `claude-4.5-opus-high-thinking`, etc.) plus its own
`composer-1` house model, none of which match the canonical LiteLLM
pricing keys (`claude-sonnet-4-6`, `claude-opus-4-5`). The alias map in
`src/models.ts` filled some of these in v0.9.4 but missed the plain
no-suffix forms (`claude-4.5-opus`, `claude-4.5-sonnet`,
`claude-4.6-opus`), the haiku tier, the forward-looking 4.7 variant,
and `composer-1`. The dashboard rendered $0 for sessions that used any
unaliased model. Visible to users in #159 even after the v0.9.4 fix.
Every Cursor variant in `src/providers/cursor.ts:modelDisplayNames`
now has an alias and a regression test asserting non-zero pricing
resolution. Closes #159.
- **Activity classifier no longer mislabels feature work as debugging.**
Messages like "add error handling", "create an issue tracker", or
"implement the 404 page" used to land in the Debugging bucket because
Expand Down
45 changes: 41 additions & 4 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,49 @@ const BUILTIN_ALIASES: Record<string, string> = {
'cline-auto': 'claude-sonnet-4-5',
'openclaw-auto': 'claude-sonnet-4-5',
'qwen-auto': 'claude-sonnet-4-5',
// Cursor emits dot-version tier-last names
'claude-4.6-sonnet': 'claude-sonnet-4-6',
'claude-4.5-sonnet-thinking': 'claude-sonnet-4-5',
// Cursor emits dot-version tier-last names plus tier/reasoning suffixes
// that LiteLLM does not index (`-high`, `-low`, `-medium`, `-thinking`,
// `-high-thinking`, `-fast-mode`). Missing aliases here surface as $0 in
// the dashboard for users on non-Auto models (issue #159). Sources: the
// display map at `src/providers/cursor.ts:modelDisplayNames`, Cursor's
// public model docs at https://cursor.com/docs/models, and forum bug
// reports that quote literal slugs (e.g. forum.cursor.com/t/154933).
'claude-4-sonnet': 'claude-sonnet-4',
'claude-4-sonnet-1m': 'claude-sonnet-4',
'claude-4-sonnet-thinking': 'claude-sonnet-4-5',
'claude-4-opus': 'claude-opus-4-5',
'claude-4.5-sonnet': 'claude-sonnet-4-5',
'claude-4.5-sonnet-thinking': 'claude-sonnet-4-5',
'claude-4.6-sonnet': 'claude-sonnet-4-6',
'claude-4.6-sonnet-high': 'claude-sonnet-4-6',
'claude-4.6-sonnet-low': 'claude-sonnet-4-6',
'claude-4.6-sonnet-thinking': 'claude-sonnet-4-6',
'claude-4.6-sonnet-high-thinking':'claude-sonnet-4-6',
'claude-4-opus': 'claude-opus-4',
'claude-4.5-opus': 'claude-opus-4-5',
'claude-4.5-opus-high': 'claude-opus-4-5',
'claude-4.5-opus-low': 'claude-opus-4-5',
'claude-4.5-opus-medium': 'claude-opus-4-5',
'claude-4.5-opus-high-thinking': 'claude-opus-4-5',
'claude-4.6-opus': 'claude-opus-4-6',
'claude-4.6-opus-fast-mode': 'claude-opus-4-6',
'claude-4.6-opus-high': 'claude-opus-4-6',
'claude-4.6-opus-low': 'claude-opus-4-6',
'claude-4.6-opus-medium': 'claude-opus-4-6',
'claude-4.6-opus-high-thinking': 'claude-opus-4-6',
'claude-4.7-opus': 'claude-opus-4-7',
// Dash form (NOT dot) seen in forum.cursor.com/t/158597.
'claude-opus-4-7-thinking-high': 'claude-opus-4-7',
'claude-4.5-haiku': 'claude-haiku-4-5',
'claude-4.6-haiku': 'claude-haiku-4-5',
// Cursor's house models have no LiteLLM pricing entry. composer-1 is
// sonnet-4.5-class per Cursor docs; composer-2 is built on Sonnet 4.6
// per cursor.com/blog/composer-2.
'composer-1': 'claude-sonnet-4-5',
'composer-1.5': 'claude-sonnet-4-5',
'composer-2': 'claude-sonnet-4-6',
// Cursor's "fast" routing variant of GPT-5 is the same model behind a
// lower-latency endpoint; price as base GPT-5 until LiteLLM tracks it.
'gpt-5-fast': 'gpt-5',
'gpt-4.1': 'gpt-4.1',
'gpt-5.2-low': 'gpt-5',
'gpt-5.1-codex-high': 'gpt-5.3-codex',
Expand Down
69 changes: 69 additions & 0 deletions tests/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,72 @@ describe('existing model names still resolve', () => {
expect(getModelCosts('anthropic/claude-opus-4-6')).not.toBeNull()
})
})

// Issue #159: every model name Cursor emits in its SQLite database must
// resolve to a non-zero pricing entry, otherwise the dashboard shows $0 for
// that model. Each case asserts the resolved pricing identity matches the
// pricing of the expected canonical key, so an accidental alias swap (e.g.
// `claude-4.6-opus` aliased to a haiku entry) fails the test even though
// haiku also has positive pricing.
describe('Cursor model variants resolve to pricing', () => {
const cases: Array<[string, string]> = [
// Sonnet family
['claude-4-sonnet', 'claude-sonnet-4'],
['claude-4-sonnet-1m', 'claude-sonnet-4'],
['claude-4-sonnet-thinking', 'claude-sonnet-4-5'],
['claude-4.5-sonnet', 'claude-sonnet-4-5'],
['claude-4.5-sonnet-thinking', 'claude-sonnet-4-5'],
['claude-4.6-sonnet', 'claude-sonnet-4-6'],
['claude-4.6-sonnet-high', 'claude-sonnet-4-6'],
['claude-4.6-sonnet-low', 'claude-sonnet-4-6'],
['claude-4.6-sonnet-thinking', 'claude-sonnet-4-6'],
['claude-4.6-sonnet-high-thinking', 'claude-sonnet-4-6'],
// Opus family
['claude-4-opus', 'claude-opus-4'],
['claude-4.5-opus', 'claude-opus-4-5'],
['claude-4.5-opus-high', 'claude-opus-4-5'],
['claude-4.5-opus-low', 'claude-opus-4-5'],
['claude-4.5-opus-medium', 'claude-opus-4-5'],
['claude-4.5-opus-high-thinking', 'claude-opus-4-5'],
['claude-4.6-opus', 'claude-opus-4-6'],
['claude-4.6-opus-fast-mode', 'claude-opus-4-6'],
['claude-4.6-opus-high', 'claude-opus-4-6'],
['claude-4.6-opus-low', 'claude-opus-4-6'],
['claude-4.6-opus-medium', 'claude-opus-4-6'],
['claude-4.6-opus-high-thinking', 'claude-opus-4-6'],
['claude-4.7-opus', 'claude-opus-4-7'],
['claude-opus-4-7-thinking-high', 'claude-opus-4-7'],
// Haiku family
['claude-4.5-haiku', 'claude-haiku-4-5'],
['claude-4.6-haiku', 'claude-haiku-4-5'],
// Cursor house models
['composer-1', 'claude-sonnet-4-5'],
['composer-1.5', 'claude-sonnet-4-5'],
['composer-2', 'claude-sonnet-4-6'],
['cursor-auto', 'claude-sonnet-4-5'],
// OpenAI variants Cursor emits
['gpt-5', 'gpt-5'],
['gpt-5-fast', 'gpt-5'],
['gpt-5.2', 'gpt-5.2'],
['gpt-5.2-low', 'gpt-5'],
// Direct LiteLLM hits where no alias is required
['grok-code-fast-1', 'grok-code-fast-1'],
['gemini-3-pro', 'gemini-3-pro-preview'],
]

for (const [input, expectedAlias] of cases) {
it(`${input} resolves to ${expectedAlias} pricing`, () => {
const costs = getModelCosts(input)
expect(costs, `${input} should resolve to pricing (and not produce $0 in the dashboard)`).not.toBeNull()
expect(costs!.inputCostPerToken).toBeGreaterThan(0)
expect(costs!.outputCostPerToken).toBeGreaterThan(0)
const expected = getModelCosts(expectedAlias)
expect(expected, `expected target ${expectedAlias} should itself resolve`).not.toBeNull()
// Identity check: the alias must produce the SAME pricing object as
// the canonical key, not just any non-zero pricing. Catches drift
// where a future edit re-points an alias at a wrong-but-positive entry.
expect(costs!.inputCostPerToken).toBe(expected!.inputCostPerToken)
expect(costs!.outputCostPerToken).toBe(expected!.outputCostPerToken)
})
}
})
Loading