Before submitting your bug report
Relevant environment info
- OS: any
- Continue version:
main @ 5522c6f
- Model:
gpt-4.1, gpt-4.1-mini, gpt-4.1-nano (any gpt-4.1* id)
- Surface: CLI cost display + telemetry, and the GUI usage panel
Description
calculateOpenAICost picks a pricing row by longest-prefix startsWith. "gpt-4.1" does not start with "gpt-4o", so the loop falls through to the legacy "gpt-4" row — the most expensive row in the table.
core/llm/utils/calculateRequestCost.ts:159-185:
"gpt-4o-mini": { input: 0.15, output: 0.6 },
"gpt-4o": { input: 2.5, output: 10 },
"gpt-4-turbo": { input: 10, output: 30 },
...
"gpt-4": { input: 30, output: 60 }, // <- GPT-4 (2023) list price
const sortedKeys = Object.keys(pricing).sort((a, b) => b.length - a.length);
for (const prefix of sortedKeys) {
if (normalizedModel.startsWith(prefix)) { modelPricing = pricing[prefix]; break }
}
"gpt-4.1".startsWith("gpt-4") is true, and nothing longer matches first. The gpt-4o family is unaffected — "gpt-4o" is checked before "gpt-4" and wins.
To reproduce
The table and matcher below are copied verbatim from calculateOpenAICost; real prices are OpenAI's list rates from https://developers.openai.com/api/docs/pricing, checked today.
const pricing = {
"gpt-4o-mini": { input: 0.15, output: 0.6 },
"gpt-4o": { input: 2.5, output: 10 },
"gpt-4-turbo": { input: 10, output: 30 },
"gpt-3.5-turbo-0125": { input: 0.5, output: 1.5 },
"gpt-3.5-turbo-1106": { input: 1, output: 2 },
"gpt-3.5-turbo": { input: 1.5, output: 2 },
"gpt-4": { input: 30, output: 60 },
};
const sortedKeys = Object.keys(pricing).sort((a, b) => b.length - a.length);
const match = (m) => {
const n = m.toLowerCase();
for (const p of sortedKeys) if (n.startsWith(p)) return [p, pricing[p]];
return [null, null];
};
for (const m of ["gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4.1-2025-04-14", "gpt-4o"])
console.log(m, "->", match(m)[0], JSON.stringify(match(m)[1]));
gpt-4.1 -> gpt-4 {"input":30,"output":60}
gpt-4.1-mini -> gpt-4 {"input":30,"output":60}
gpt-4.1-nano -> gpt-4 {"input":30,"output":60}
gpt-4.1-2025-04-14 -> gpt-4 {"input":30,"output":60}
gpt-4o -> gpt-4o {"input":2.5,"output":10}
Expected / actual
| model |
charged in / out |
OpenAI list in / out |
overcharge |
gpt-4.1 |
$30 / $60 |
$2.00 / $8.00 |
15x input, 7.5x output |
gpt-4.1-mini |
$30 / $60 |
$0.40 / $1.60 |
75x input, 37.5x output |
gpt-4.1-nano |
$30 / $60 |
$0.10 / $0.40 |
300x input, 150x output |
A 100k-token gpt-4.1-mini prompt is reported as $3.00 against a real charge of $0.04.
The three date-stamped ids (gpt-4.1-2025-04-14 and friends) hit it too, so pinning a snapshot doesn't avoid it.
Suggested fix
Add the three rows. sortedKeys is longest-first, so "gpt-4.1-mini" (12) and "gpt-4.1-nano" (12) are checked before "gpt-4.1" (7), which is checked before "gpt-4" (5) — no matcher change needed:
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
"gpt-4.1-nano": { input: 0.1, output: 0.4 },
"gpt-4.1": { input: 2.0, output: 8.0 },
Worth considering separately: "gpt-4" is a bare-prefix catch-all at the highest price in the table, so any future gpt-4.x inherits $30/$60 silently. Requiring the next character to be - or end-of-string would send unknown variants to the null path (no cost shown / CLI fallback) instead of to the most expensive row. That is a behaviour change for unknown ids, which is why I've kept it out of the fix above.
Unrelated to this, but visible from the same table: gpt-5* ids match nothing and return null, so the GUI shows no cost for them at all.
Note on a PR
I can't sign the CLA, so this is an issue rather than a PR — the three lines above are the whole change, and the prices are linked to source. Happy to add the vitest cases as a patch in a comment if that's useful.
Before submitting your bug report
Relevant environment info
main@5522c6fgpt-4.1,gpt-4.1-mini,gpt-4.1-nano(anygpt-4.1*id)Description
calculateOpenAICostpicks a pricing row by longest-prefixstartsWith."gpt-4.1"does not start with"gpt-4o", so the loop falls through to the legacy"gpt-4"row — the most expensive row in the table.core/llm/utils/calculateRequestCost.ts:159-185:"gpt-4.1".startsWith("gpt-4")istrue, and nothing longer matches first. Thegpt-4ofamily is unaffected —"gpt-4o"is checked before"gpt-4"and wins.To reproduce
The table and matcher below are copied verbatim from
calculateOpenAICost; real prices are OpenAI's list rates from https://developers.openai.com/api/docs/pricing, checked today.Expected / actual
gpt-4.1gpt-4.1-minigpt-4.1-nanoA 100k-token
gpt-4.1-miniprompt is reported as $3.00 against a real charge of $0.04.The three date-stamped ids (
gpt-4.1-2025-04-14and friends) hit it too, so pinning a snapshot doesn't avoid it.Suggested fix
Add the three rows.
sortedKeysis longest-first, so"gpt-4.1-mini"(12) and"gpt-4.1-nano"(12) are checked before"gpt-4.1"(7), which is checked before"gpt-4"(5) — no matcher change needed:Worth considering separately:
"gpt-4"is a bare-prefix catch-all at the highest price in the table, so any futuregpt-4.xinherits $30/$60 silently. Requiring the next character to be-or end-of-string would send unknown variants to thenullpath (no cost shown / CLI fallback) instead of to the most expensive row. That is a behaviour change for unknown ids, which is why I've kept it out of the fix above.Unrelated to this, but visible from the same table:
gpt-5*ids match nothing and returnnull, so the GUI shows no cost for them at all.Note on a PR
I can't sign the CLA, so this is an issue rather than a PR — the three lines above are the whole change, and the prices are linked to source. Happy to add the vitest cases as a patch in a comment if that's useful.