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:
Aditya Vikram Singh 2026-08-21 16:45:24 +05:30
parent 21ef46523d
commit 65263da303
8 changed files with 191 additions and 38 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)) {

View file

@ -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 ?? [])
}

View file

@ -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()}`
}

View file

@ -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)
})

View file

@ -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', () => {