mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-23 23:45:12 +00:00
fix(models): classify real subscription SKUs and let --remove opt out built-ins
Maintainer review on #1050: drop metered codex-auto-review, add kimi-for-coding-highspeed, match Warp's auto id, drop unsourced big-pickle, and give model-flat-rate --remove power over built-ins.
This commit is contained in:
parent
21ef46523d
commit
65263da303
8 changed files with 191 additions and 38 deletions
|
|
@ -35,6 +35,7 @@
|
|||
- **The resident `codeburn serve` child.** The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972)
|
||||
|
||||
### Fixed
|
||||
- **Subscription SKUs are classified from real product ids, and a false-positive built-in can be opted out.** `codex-auto-review` consumes ordinary Codex usage ([openai/codex#32224](https://github.com/openai/codex/issues/32224)) and is priced as GPT-5.5 on #1056, so treating it as $0 hid real spend — it left the flat-rate list. Warp's product id is `auto`, not the synthetic `warp`. `kimi-for-coding-highspeed` (the SKU #968 was filed around) is now honestly $0. `big-pickle` was dropped: it appears under OpenCode, not as a cited ClinePass codename. `codeburn model-flat-rate --remove` now opts out of a built-in, so a wrong classifier entry can warn again without waiting for a release. The daily-cache config hash now always includes the flat-rate section (even when empty), so the first run after upgrade re-derives every stored day once from the warm session cache. (#968, #1050)
|
||||
- **Plan rows for sticker-price presets read as a budget instead of live provider quota.** There is no Grok quota endpoint, so a SuperGrok row was parsed API-equivalent spend divided by the plan's sticker price on a monthly reset — but the TUI labelled that math "plan" and "reset", which next to a client showing xAI's real weekly window read as CodeBurn being wrong. The bars and the arithmetic are unchanged; the words are not. Both the dashboard and the desktop app now say the number is an API-equivalent monthly budget and not a live provider window, in the same wording on both surfaces, and for every preset rather than as a SuperGrok special case. The window is anniversary-based (`plan.resetDay`, settable with `codeburn plan set --reset-day`), so it is called a budget reset rather than a calendar one. The row was also shortened to fit 80 columns: at that width the percentage and the projected month were being truncated away, including on custom plans, whose label carries the provider.
|
||||
- **MiMo sessions price from the LiteLLM Xiaomi rows, and MiMo v2 Flash no longer crashes the display path.** Hermes / Xiaomi token-plan sessions store the bare id (`mimo-v2.5-pro`, `mimo-v2.5`) while LiteLLM namespaces its row (`xiaomi/…`), so those models reported $0. They now alias to the existing snapshot rows — no invented rate, and `kimi-k3` still has none — which means a session Hermes left costless is priced from the shared tables and carries the estimated marker, exactly as `mimo-v2-flash` already did. The same change fixes a **pre-existing** crash that this alias did not introduce: the shipped `mimo-v2-flash -> xiaomi/mimo-v2-flash` alias already cycled through display-name resolution — strip the namespace, alias it back, take the leaf, repeat — so `getShortModelName` blew the stack on any real MiMo v2 Flash session and took every surface that names a model down with it, the `models` table included. Display-name resolution is now cycle-safe, and the `mimo-v2-flash` and `mimo-v2.5` rows are named rather than shown as raw slugs.
|
||||
- **A date-ranged run no longer republishes the month shards it never read.** A scoped load leaves an out-of-range month on disk, so the files it holds have no visible cache entry and the reconcile re-parses them — re-deriving the entry the shard already stores. That re-parse marked the unloaded month dirty, and the save merged and republished it under a fresh nonce name on every single run, byte-identical content and all, so a repeated `codeburn status --format json` churned old months (on a real corpus: claude/2026-03, cursor/2026-02 and warp/2026-03 renamed every run) and left the retired shards for the sweeper. A merge into an unloaded month that neither adds, changes nor removes an entry now keeps the published shard, so unchanged months keep their names and their bytes. (#1032)
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ codeburn model-flat-rate auto-genius # subscription SKU
|
|||
codeburn proxy-path ~/work/copilot-repo # subscription-covered project
|
||||
```
|
||||
|
||||
`price-override` sets exact rates for any model (input, output, cache read, cache creation), useful for private deployments or models LiteLLM prices wrong. `model-savings` maps a free local model to a paid baseline: the local calls stay $0, and the dashboard shows what the same tokens would have cost on the baseline. `model-flat-rate` marks a subscription-billed product SKU so the unpriced warning stays quiet and `model-alias` is not suggested — aliasing those ids invents spend. `proxy-path` marks a project routed through a subscription-backed proxy (e.g. Claude Code over GitHub Copilot), so its API-rate cost is reported as subscription-covered and your net out-of-pocket stays honest. All four support `--list` and `--remove`.
|
||||
`price-override` sets exact rates for any model (input, output, cache read, cache creation), useful for private deployments or models LiteLLM prices wrong. `model-savings` maps a free local model to a paid baseline: the local calls stay $0, and the dashboard shows what the same tokens would have cost on the baseline. `model-flat-rate` marks a subscription-billed product SKU so the unpriced warning stays quiet and `model-alias` is not suggested — aliasing those ids invents spend. `--remove` also opts out of a built-in SKU. `proxy-path` marks a project routed through a subscription-backed proxy (e.g. Claude Code over GitHub Copilot), so its API-rate cost is reported as subscription-covered and your net out-of-pocket stays honest. All four support `--list` and `--remove`.
|
||||
|
||||
### Filtering
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ export type CodeburnConfig = {
|
|||
// modelAliases (which invent per-token spend) and localModelSavings
|
||||
// (counterfactual local baseline). See `codeburn model-flat-rate`.
|
||||
flatRateModels?: string[]
|
||||
// Opt-outs from the built-in flat-rate classifier. `model-flat-rate --remove`
|
||||
// on a built-in SKU records the id here so a false positive can warn again
|
||||
// without waiting for a release.
|
||||
flatRateModelsRemoved?: string[]
|
||||
// Spend budgets are stored in the configured display currency, not USD.
|
||||
budget?: {
|
||||
daily?: number
|
||||
|
|
|
|||
51
src/main.ts
51
src/main.ts
|
|
@ -2,7 +2,7 @@ import { isAbsolute } from 'path'
|
|||
import { Command, Option } from 'commander'
|
||||
import { installMenubarApp } from './menubar-installer.js'
|
||||
import { exportCsv, exportJson, type PeriodExport } from './export.js'
|
||||
import { findUnpricedModels, loadPricing, sanitizeModelForDisplay, setModelAliases, setPriceOverrides, setLocalModelSavings, setFlatRateModels, setProxyPaths, normalizeProxyPath, unpricedModelHint } from './models.js'
|
||||
import { findUnpricedModels, loadPricing, sanitizeModelForDisplay, setModelAliases, setPriceOverrides, setLocalModelSavings, setFlatRateModels, setFlatRateRemoved, setProxyPaths, normalizeProxyPath, unpricedModelHint, isBuiltInFlatRateModel, isSameFlatRateModel } from './models.js'
|
||||
import { parseAllSessions, filterProjectsByName, filterProjectsByDateRange, clearSessionCache, setInteractiveScanUI } from './parser.js'
|
||||
import { allProviderNames, getAllProviders } from './providers/index.js'
|
||||
import { getProvider } from './providers/index.js'
|
||||
|
|
@ -466,6 +466,7 @@ program.hook('preAction', async (thisCommand) => {
|
|||
setPriceOverrides(config.priceOverrides ?? {})
|
||||
setLocalModelSavings(config.localModelSavings ?? {})
|
||||
setFlatRateModels(config.flatRateModels ?? [])
|
||||
setFlatRateRemoved(config.flatRateModelsRemoved ?? [])
|
||||
setProxyPaths(config.proxyPaths ?? [])
|
||||
if (thisCommand.opts<{ verbose?: boolean }>().verbose) {
|
||||
process.env['CODEBURN_VERBOSE'] = '1'
|
||||
|
|
@ -1582,21 +1583,30 @@ program
|
|||
program
|
||||
.command('model-flat-rate [model]')
|
||||
.description('Mark a model as subscription / flat-rate billed. $0 is the correct cost and the unpriced warning is silenced. Do not use model-alias for these — that maps them onto another model\'s per-token rate and invents spend (e.g. codeburn model-flat-rate auto-genius).')
|
||||
.option('--remove <model>', 'Remove a flat-rate mark')
|
||||
.option('--list', 'List configured flat-rate models')
|
||||
.option('--remove <model>', 'Remove a flat-rate mark, including a built-in SKU')
|
||||
.option('--list', 'List configured flat-rate models and built-in opt-outs')
|
||||
.action(async (model?: string, opts?: { remove?: string; list?: boolean }) => {
|
||||
const config = await readConfig()
|
||||
const marked = [...(config.flatRateModels ?? [])]
|
||||
const removed = [...(config.flatRateModelsRemoved ?? [])]
|
||||
|
||||
if (opts?.list || (!model && !opts?.remove)) {
|
||||
if (marked.length === 0) {
|
||||
if (marked.length === 0 && removed.length === 0) {
|
||||
console.log('\n No flat-rate models configured.')
|
||||
console.log(` Config: ${getConfigFilePath()}`)
|
||||
console.log(' Add one with: codeburn model-flat-rate <model>\n')
|
||||
} else {
|
||||
console.log('\n Flat-rate / subscription models:')
|
||||
for (const name of marked) {
|
||||
console.log(` ${name}`)
|
||||
if (marked.length > 0) {
|
||||
console.log('\n Flat-rate / subscription models:')
|
||||
for (const name of marked) {
|
||||
console.log(` ${name}`)
|
||||
}
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
console.log('\n Built-in flat-rate opt-outs (unpriced warning fires again):')
|
||||
for (const name of removed) {
|
||||
console.log(` ${name}`)
|
||||
}
|
||||
}
|
||||
console.log(` Config: ${getConfigFilePath()}\n`)
|
||||
}
|
||||
|
|
@ -1604,16 +1614,29 @@ program
|
|||
}
|
||||
|
||||
if (opts?.remove) {
|
||||
const idx = marked.indexOf(opts.remove)
|
||||
if (idx < 0) {
|
||||
console.error(`\n No flat-rate mark found for: ${opts.remove}\n`)
|
||||
const target = opts.remove
|
||||
const idx = marked.indexOf(target)
|
||||
const builtIn = isBuiltInFlatRateModel(target)
|
||||
const alreadyOptedOut = removed.some(id => isSameFlatRateModel(id, target))
|
||||
if (idx < 0 && (!builtIn || alreadyOptedOut)) {
|
||||
console.error(`\n No flat-rate mark found for: ${target}\n`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
marked.splice(idx, 1)
|
||||
config.flatRateModels = marked.length > 0 ? marked : undefined
|
||||
if (idx >= 0) {
|
||||
marked.splice(idx, 1)
|
||||
config.flatRateModels = marked.length > 0 ? marked : undefined
|
||||
}
|
||||
if (builtIn && !alreadyOptedOut) {
|
||||
removed.push(target)
|
||||
config.flatRateModelsRemoved = removed
|
||||
}
|
||||
await saveConfig(config)
|
||||
console.log(`\n Removed flat-rate mark: ${opts.remove}\n`)
|
||||
console.log(`\n Removed flat-rate mark: ${target}`)
|
||||
if (builtIn) {
|
||||
console.log(' Built-in SKU opted out; the unpriced warning will fire again until you re-add it.')
|
||||
}
|
||||
console.log()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1625,6 +1648,8 @@ program
|
|||
|
||||
if (!marked.includes(model)) marked.push(model)
|
||||
config.flatRateModels = marked
|
||||
const remainingOptOuts = removed.filter(id => !isSameFlatRateModel(id, model))
|
||||
config.flatRateModelsRemoved = remainingOptOuts.length > 0 ? remainingOptOuts : undefined
|
||||
await saveConfig(config)
|
||||
|
||||
if (config.modelAliases && Object.hasOwn(config.modelAliases, model)) {
|
||||
|
|
|
|||
|
|
@ -526,6 +526,8 @@ export function getLocalModelSavingsConfigHash(): string {
|
|||
// price-override (user-declared free). Built-in families plus a user hatch.
|
||||
let userFlatRateModels = new Set<string>()
|
||||
let userFlatRateLeaves = new Set<string>()
|
||||
let userFlatRateRemoved = new Set<string>()
|
||||
let userFlatRateRemovedLeaves = new Set<string>()
|
||||
|
||||
function flatRateLeaf(model: string): string {
|
||||
const trimmed = model.trim().replace(/@.*$/, '').replace(/-\d{8}$/, '')
|
||||
|
|
@ -533,46 +535,82 @@ function flatRateLeaf(model: string): string {
|
|||
return leaf.toLowerCase()
|
||||
}
|
||||
|
||||
export function setFlatRateModels(models: Iterable<string>): void {
|
||||
userFlatRateModels = new Set()
|
||||
userFlatRateLeaves = new Set()
|
||||
function fillFlatRateSet(
|
||||
models: Iterable<string>,
|
||||
): { ids: Set<string>; leaves: Set<string> } {
|
||||
const ids = new Set<string>()
|
||||
const leaves = new Set<string>()
|
||||
for (const model of models) {
|
||||
if (!model || typeof model !== 'string') continue
|
||||
userFlatRateModels.add(model)
|
||||
ids.add(model)
|
||||
const leaf = flatRateLeaf(model)
|
||||
if (leaf) userFlatRateLeaves.add(leaf)
|
||||
if (leaf) leaves.add(leaf)
|
||||
}
|
||||
return { ids, leaves }
|
||||
}
|
||||
|
||||
export function setFlatRateModels(models: Iterable<string>): void {
|
||||
const filled = fillFlatRateSet(models)
|
||||
userFlatRateModels = filled.ids
|
||||
userFlatRateLeaves = filled.leaves
|
||||
}
|
||||
|
||||
export function setFlatRateRemoved(models: Iterable<string>): void {
|
||||
const filled = fillFlatRateSet(models)
|
||||
userFlatRateRemoved = filled.ids
|
||||
userFlatRateRemovedLeaves = filled.leaves
|
||||
}
|
||||
|
||||
export function getFlatRateModelsConfigHash(): string {
|
||||
return [...userFlatRateModels].sort().join('\u0002')
|
||||
const added = [...userFlatRateModels].sort().join('\u0002')
|
||||
const removed = [...userFlatRateRemoved].sort().join('\u0002')
|
||||
if (!removed) return added
|
||||
return `${added}\u0003${removed}`
|
||||
}
|
||||
|
||||
export function getFlatRateModels(): string[] {
|
||||
return [...userFlatRateModels]
|
||||
}
|
||||
|
||||
export function getFlatRateRemoved(): string[] {
|
||||
return [...userFlatRateRemoved]
|
||||
}
|
||||
|
||||
export function isSameFlatRateModel(a: string, b: string): boolean {
|
||||
if (!a || !b) return false
|
||||
if (a === b) return true
|
||||
const leaf = flatRateLeaf(a)
|
||||
return leaf.length > 0 && leaf === flatRateLeaf(b)
|
||||
}
|
||||
|
||||
function isUserFlatRateModel(model: string): boolean {
|
||||
if (userFlatRateModels.has(model)) return true
|
||||
const leaf = flatRateLeaf(model)
|
||||
return leaf.length > 0 && userFlatRateLeaves.has(leaf)
|
||||
}
|
||||
|
||||
/// Product SKUs billed as a subscription, not missing LiteLLM rows.
|
||||
/// Match raw ids, path-prefixed ids (`cline-pass/auto-genius`), and the
|
||||
/// display names aggregation keys by (parser.ts uses getShortModelName).
|
||||
function isBuiltInFlatRateModel(model: string): boolean {
|
||||
function isFlatRateRemoved(model: string): boolean {
|
||||
if (userFlatRateRemoved.has(model)) return true
|
||||
const leaf = flatRateLeaf(model)
|
||||
return leaf.length > 0 && userFlatRateRemovedLeaves.has(leaf)
|
||||
}
|
||||
|
||||
/// Product SKUs billed as a subscription, not missing LiteLLM rows.
|
||||
/// Match raw ids and path-prefixed ids (`cline-pass/auto-genius`). Display
|
||||
/// names from getShortModelName are matched only when the aggregation key
|
||||
/// is not the raw leaf (Warp Auto *, Grok Composer *).
|
||||
export function isBuiltInFlatRateModel(model: string): boolean {
|
||||
const leaf = flatRateLeaf(model)
|
||||
// Warp's product SKU is the bare id `auto`. Kiro rewrites its own `auto`
|
||||
// to `kiro-auto` before pricing, so this leaf does not swallow Kiro.
|
||||
if (
|
||||
leaf === 'warp'
|
||||
|| leaf === 'codex-auto-review'
|
||||
leaf === 'auto'
|
||||
|| leaf === 'auto-genius'
|
||||
|| leaf === 'big-pickle'
|
||||
|| leaf === 'kimi-for-coding-highspeed'
|
||||
) return true
|
||||
if (leaf.startsWith('grok-composer-')) return true
|
||||
if (leaf.startsWith('warp-auto-')) return true
|
||||
const display = model.trim()
|
||||
if (/^codex auto review$/i.test(display)) return true
|
||||
if (/^grok composer\b/i.test(display)) return true
|
||||
if (/^warp auto\b/i.test(display)) return true
|
||||
return false
|
||||
|
|
@ -580,6 +618,7 @@ function isBuiltInFlatRateModel(model: string): boolean {
|
|||
|
||||
export function isFlatRateModel(model: string): boolean {
|
||||
if (!model) return false
|
||||
if (isFlatRateRemoved(model)) return false
|
||||
return isUserFlatRateModel(model) || isBuiltInFlatRateModel(model)
|
||||
}
|
||||
|
||||
|
|
@ -966,7 +1005,7 @@ function shouldWarnAboutUnknownModel(name: string): boolean {
|
|||
// inference can still set an alias via `codeburn model-alias`.
|
||||
if (looksLikeLocalModel(name)) return false
|
||||
if (isFlatRateModel(name)) return false
|
||||
// The warning fired on every CLI invocation (including the default)
|
||||
// The warning fired on every CLI invocation (including the default
|
||||
// dashboard) which made first launches look broken — three "no pricing
|
||||
// data" lines greet a user before the dashboard even draws. Now opt-in
|
||||
// via --verbose. The unknown model still costs $0 in reports; users who
|
||||
|
|
@ -1235,6 +1274,7 @@ export type PricingSnapshot = {
|
|||
priceOverrides: Record<string, PriceOverrideRates>
|
||||
localModelSavings: Record<string, string>
|
||||
flatRateModels?: string[]
|
||||
flatRateModelsRemoved?: string[]
|
||||
}
|
||||
|
||||
export function snapshotPricingState(): PricingSnapshot {
|
||||
|
|
@ -1244,6 +1284,7 @@ export function snapshotPricingState(): PricingSnapshot {
|
|||
priceOverrides: userPriceOverridesConfig,
|
||||
localModelSavings: userLocalModelSavings,
|
||||
flatRateModels: getFlatRateModels(),
|
||||
flatRateModelsRemoved: getFlatRateRemoved(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1256,4 +1297,5 @@ export function restorePricingState(snapshot: PricingSnapshot): void {
|
|||
setPriceOverrides(snapshot.priceOverrides)
|
||||
setLocalModelSavings(snapshot.localModelSavings)
|
||||
setFlatRateModels(snapshot.flatRateModels ?? [])
|
||||
setFlatRateRemoved(snapshot.flatRateModelsRemoved ?? [])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3504,6 +3504,9 @@ function cacheKey(dateRange: DateRange | undefined, providerFilter: string | und
|
|||
// Pricing-affecting config participates so a memoized parse (exact-key or
|
||||
// burst-reused in a resident serve process) can never present costs priced
|
||||
// under aliases/overrides/savings the user has since changed.
|
||||
// Flat-rate marks do not change parse-time cost (still $0 without a LiteLLM
|
||||
// row); findUnpricedModels / coverage apply them at render time, so they
|
||||
// stay out of this serve-memo key on purpose.
|
||||
return `${s}:${providerFilter ?? 'all'}:${claudeRoots}:${getProxyPathsConfigHash()}:${getModelAliasesConfigHash()}:${getPriceOverridesConfigHash()}:${getLocalModelSavingsConfigHash()}`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process'
|
|||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
const CLI_TIMEOUT_MS = 10_000
|
||||
const CLI_TIMEOUT_MS = 30_000
|
||||
|
||||
function runCli(args: string[], home: string) {
|
||||
return spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], {
|
||||
|
|
@ -30,22 +30,23 @@ describe('codeburn model-flat-rate command', () => {
|
|||
it('saves, lists, and removes a flat-rate mark', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-flat-rate-'))
|
||||
try {
|
||||
const set = runCli(['model-flat-rate', 'auto-genius'], home)
|
||||
const set = runCli(['model-flat-rate', 'zz-my-pass-sku'], home)
|
||||
expect(set.status).toBe(0)
|
||||
expect(set.stdout).toContain('Flat-rate mark saved: auto-genius')
|
||||
expect(set.stdout).toContain('Flat-rate mark saved: zz-my-pass-sku')
|
||||
|
||||
const saved = await readConfig(home)
|
||||
expect(saved.flatRateModels).toEqual(['auto-genius'])
|
||||
expect(saved.flatRateModels).toEqual(['zz-my-pass-sku'])
|
||||
|
||||
const list = runCli(['model-flat-rate', '--list'], home)
|
||||
expect(list.status).toBe(0)
|
||||
expect(list.stdout).toContain('auto-genius')
|
||||
expect(list.stdout).toContain('zz-my-pass-sku')
|
||||
|
||||
const remove = runCli(['model-flat-rate', '--remove', 'auto-genius'], home)
|
||||
const remove = runCli(['model-flat-rate', '--remove', 'zz-my-pass-sku'], home)
|
||||
expect(remove.status).toBe(0)
|
||||
|
||||
const after = await readConfig(home)
|
||||
expect(after.flatRateModels).toBeUndefined()
|
||||
expect(after.flatRateModelsRemoved).toBeUndefined()
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
|
|
@ -74,4 +75,31 @@ describe('codeburn model-flat-rate command', () => {
|
|||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
}, CLI_TIMEOUT_MS)
|
||||
|
||||
it('opts out of a built-in SKU so the unpriced warning can fire again', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-flat-rate-'))
|
||||
try {
|
||||
const remove = runCli(['model-flat-rate', '--remove', 'auto-genius'], home)
|
||||
expect(remove.status).toBe(0)
|
||||
expect(remove.stdout).toContain('Removed flat-rate mark: auto-genius')
|
||||
expect(remove.stdout).toContain('Built-in SKU opted out')
|
||||
|
||||
const saved = await readConfig(home)
|
||||
expect(saved.flatRateModels).toBeUndefined()
|
||||
expect(saved.flatRateModelsRemoved).toEqual(['auto-genius'])
|
||||
|
||||
const list = runCli(['model-flat-rate', '--list'], home)
|
||||
expect(list.status).toBe(0)
|
||||
expect(list.stdout).toContain('auto-genius')
|
||||
expect(list.stdout).toContain('Built-in flat-rate opt-outs')
|
||||
|
||||
const restore = runCli(['model-flat-rate', 'auto-genius'], home)
|
||||
expect(restore.status).toBe(0)
|
||||
const after = await readConfig(home)
|
||||
expect(after.flatRateModels).toEqual(['auto-genius'])
|
||||
expect(after.flatRateModelsRemoved).toBeUndefined()
|
||||
} finally {
|
||||
await rm(home, { recursive: true, force: true })
|
||||
}
|
||||
}, CLI_TIMEOUT_MS)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
setPriceOverrides,
|
||||
setLocalModelSavings,
|
||||
setFlatRateModels,
|
||||
setFlatRateRemoved,
|
||||
isExpectedFreeModel,
|
||||
isFlatRateModel,
|
||||
getLocalModelSavingsConfigHash,
|
||||
|
|
@ -33,6 +34,7 @@ afterEach(() => {
|
|||
setPriceOverrides({})
|
||||
setLocalModelSavings({})
|
||||
setFlatRateModels([])
|
||||
setFlatRateRemoved([])
|
||||
})
|
||||
|
||||
describe('getModelCosts', () => {
|
||||
|
|
@ -1000,16 +1002,25 @@ describe('findUnpricedModels', () => {
|
|||
it('skips subscription / flat-rate product SKUs where $0 is correct', () => {
|
||||
const rows = [
|
||||
{ model: 'auto-genius', calls: 898, cost: 0, tokens: 35_300_000 },
|
||||
{ model: 'cline-pass/big-pickle', calls: 4, cost: 0, tokens: 33_900 },
|
||||
{ model: 'warp', calls: 449, cost: 0, tokens: 17_700_000 },
|
||||
{ model: 'codex-auto-review', calls: 940, cost: 0, tokens: 7_200_000 },
|
||||
{ model: 'cline-pass/auto-genius', calls: 4, cost: 0, tokens: 33_900 },
|
||||
{ model: 'auto', calls: 449, cost: 0, tokens: 17_700_000 },
|
||||
{ model: 'kimi-for-coding-highspeed', calls: 12, cost: 0, tokens: 3_400_000 },
|
||||
{ model: 'moonshot/kimi-for-coding-highspeed', calls: 2, cost: 0, tokens: 80_000 },
|
||||
{ model: 'grok-composer-2.5-fast', calls: 10, cost: 0, tokens: 1_900_000 },
|
||||
{ model: 'Grok Composer 2.5 Fast', calls: 10, cost: 0, tokens: 1_900_000 },
|
||||
{ model: 'Warp Auto (efficient)', calls: 3, cost: 0, tokens: 50_000 },
|
||||
{ model: 'warp', calls: 449, cost: 0, tokens: 17_700_000 },
|
||||
{ model: 'codex-auto-review', calls: 940, cost: 0, tokens: 7_200_000 },
|
||||
{ model: 'Codex Auto Review', calls: 2, cost: 0, tokens: 100 },
|
||||
{ model: 'big-pickle', calls: 4, cost: 0, tokens: 33_900 },
|
||||
{ model: 'zz-mystery-paid-model-999', calls: 3, cost: 0, tokens: 1200 },
|
||||
]
|
||||
expect(findUnpricedModels(rows)).toEqual([
|
||||
{ model: 'warp', calls: 449, tokens: 17_700_000 },
|
||||
{ model: 'codex-auto-review', calls: 940, tokens: 7_200_000 },
|
||||
{ model: 'big-pickle', calls: 4, tokens: 33_900 },
|
||||
{ model: 'zz-mystery-paid-model-999', calls: 3, tokens: 1200 },
|
||||
{ model: 'Codex Auto Review', calls: 2, tokens: 100 },
|
||||
])
|
||||
})
|
||||
|
||||
|
|
@ -1029,9 +1040,26 @@ describe('findUnpricedModels', () => {
|
|||
expect(getModelCosts('warp-auto-efficient')).not.toBeNull()
|
||||
expect(isExpectedFreeModel('warp-auto-efficient')).toBe(false)
|
||||
expect(isExpectedFreeModel('auto-genius')).toBe(true)
|
||||
expect(isExpectedFreeModel('auto')).toBe(true)
|
||||
expect(isExpectedFreeModel('kimi-for-coding-highspeed')).toBe(true)
|
||||
expect(isExpectedFreeModel('warp')).toBe(false)
|
||||
expect(isExpectedFreeModel('codex-auto-review')).toBe(false)
|
||||
expect(isExpectedFreeModel('zz-mystery-paid-model-999')).toBe(false)
|
||||
})
|
||||
|
||||
it('lets --remove opt out of a built-in so a false positive can warn again', () => {
|
||||
expect(findUnpricedModels([{ model: 'auto-genius', calls: 1, cost: 0, tokens: 10 }])).toEqual([])
|
||||
setFlatRateRemoved(['auto-genius'])
|
||||
expect(isFlatRateModel('auto-genius')).toBe(false)
|
||||
expect(findUnpricedModels([{ model: 'auto-genius', calls: 1, cost: 0, tokens: 10 }])).toEqual([
|
||||
{ model: 'auto-genius', calls: 1, tokens: 10 },
|
||||
])
|
||||
expect(findUnpricedModels([{ model: 'cline-pass/auto-genius', calls: 1, cost: 0, tokens: 10 }])).toEqual([
|
||||
{ model: 'cline-pass/auto-genius', calls: 1, tokens: 10 },
|
||||
])
|
||||
expect(isFlatRateModel('auto')).toBe(true)
|
||||
})
|
||||
|
||||
it('sorts by tokens, then calls', () => {
|
||||
const unpriced = findUnpricedModels([
|
||||
{ model: 'zz-small', calls: 9, cost: 0, tokens: 10 },
|
||||
|
|
@ -1086,6 +1114,16 @@ describe('getFlatRateModelsConfigHash', () => {
|
|||
expect(getFlatRateModelsConfigHash()).toBe(two)
|
||||
setFlatRateModels([])
|
||||
})
|
||||
|
||||
it('changes when a built-in is opted out', () => {
|
||||
setFlatRateModels([])
|
||||
setFlatRateRemoved([])
|
||||
const baseline = getFlatRateModelsConfigHash()
|
||||
setFlatRateRemoved(['auto-genius'])
|
||||
expect(getFlatRateModelsConfigHash()).not.toBe(baseline)
|
||||
setFlatRateRemoved([])
|
||||
expect(getFlatRateModelsConfigHash()).toBe(baseline)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pricing snapshot carries flat-rate marks', () => {
|
||||
|
|
@ -1100,6 +1138,18 @@ describe('pricing snapshot carries flat-rate marks', () => {
|
|||
expect(isFlatRateModel('zz-snapshot-flat')).toBe(true)
|
||||
setFlatRateModels([])
|
||||
})
|
||||
|
||||
it('restorePricingState reapplies built-in opt-outs', async () => {
|
||||
const { snapshotPricingState, restorePricingState } = await import('../src/models.js')
|
||||
setFlatRateRemoved(['auto-genius'])
|
||||
const snap = snapshotPricingState()
|
||||
expect(snap.flatRateModelsRemoved).toEqual(['auto-genius'])
|
||||
setFlatRateRemoved([])
|
||||
expect(isFlatRateModel('auto-genius')).toBe(true)
|
||||
restorePricingState(snap)
|
||||
expect(isFlatRateModel('auto-genius')).toBe(false)
|
||||
setFlatRateRemoved([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('unpricedModelHint', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue