catalog, gui: a 1-hour cache write is 2× input for a Claude model only, any other's unset is its 5-minute price and isn't shown (PAMI on Discord)

v0.1.1032 counted a 1-hour cache write left out at 2× input for every
model, so gpt-6-astra priced 10,50,1,12.5 showed "$20 (1 hour) at 2×
input", a write OpenAI has no price for. OneHourFor gives a Claude model's
price, listed or given by the user, its 2× input; any other model's 1-hour
writes count at the 5-minute price, magpie model price shows only its cache
write, and the app shows the 1-hour box only for a Claude model or one
priced with one.
This commit is contained in:
yetone committed 2026-10-05 22:55:30 +08:00
1 parent a532d1586c
commit 3a30ac87c2
11 files changed
+77 -24

No files matched your search

+4 -2
View File
@@ -456,8 +456,10 @@ write kept for 5 minutes at 1.25× input and one kept for an hour at 2× input,
and its usage says which were which (`cache_creation.ephemeral_5m_input_tokens`
/ `ephemeral_1h_input_tokens`, from the API and from Claude Code's
transcripts). The fourth number is the 5-minute price; the fifth, when not
given, is 2× input — Anthropic's rule, and only Anthropic reports 1-hour
writes, so for any other model it never comes into play. A call recorded
given, is 2× input for a Claude model — Anthropic's rule. Any other model has
no 1-hour price unless you give one: a 1-hour write it reports is counted at
the 5-minute price, and neither `magpie model price` nor the app's boxes show
a 1-hour price for it. A call recorded
before magpie kept the split counts all its writes at the 5-minute price, as
it did.
+12 -9
View File
@@ -105,8 +105,8 @@ type Price struct {
// CacheWrite1h is what a cache write kept for an hour costs, Anthropic's
// 1-hour TTL, which it bills at 2× input where a 5-minute write (the
// CacheWrite above) is 1.25×. 0 is not given: such a write is then
// counted at 2× input, Anthropic's rule for every model, as only
// Anthropic's usage says which writes were for an hour.
// counted at the 5-minute price, as no other vendor has an hour's
// writes to bill apart; a Claude model's is 2× input (OneHourFor).
CacheWrite1h float64 `json:"cache_write_1h,omitempty"`
// Tiers are the prices a request is billed at, whole, once its input
// goes over a size (OpenAI's gpt-6-astra: 2× input and cache, 1.5×
@@ -177,10 +177,13 @@ func (p *Price) UnmarshalJSON(b []byte) error {
return nil
}
// claudeOneHour gives a Claude model's list price its 1-hour cache write,
// which models.dev doesn't list: 2× input, as Anthropic bills it for every
// model, wherever the model has a (5-minute) cache write price at all.
func claudeOneHour(id string, p *Price) {
// OneHourFor gives a Claude model's price its 1-hour cache write where none
// is given — models.dev lists none, and a price the user gives may leave it
// out: 2× input, as Anthropic bills it for every model, wherever the model
// has a (5-minute) cache write price at all. Any other model's stays
// unset, its writes all at the 5-minute price: only Anthropic has a 1-hour
// one (PAMI on Discord: gpt-6-astra was shown a $20 one that isn't).
func OneHourFor(id string, p *Price) {
if p == nil || p.CacheWrite <= 0 || p.CacheWrite1h > 0 || !strings.Contains(strings.ToLower(id), "claude") {
return
}
@@ -222,12 +225,12 @@ func (p Price) At(prompt int) Price {
}
// OneHour is what a 1-hour cache write costs at this flat price: the price
// given, else 2× input, Anthropic's rule.
// given, else the 5-minute one (a Claude model's is given, OneHourFor).
func (p Price) OneHour() float64 {
if p.CacheWrite1h > 0 {
return p.CacheWrite1h
}
return 2 * p.Input
return p.CacheWrite
}
// Cost of a call at this price, every cache write a 5-minute one.
@@ -370,7 +373,7 @@ func load() map[string]mdProvider {
voted := map[string]bool{}
for pid, p := range m {
for id, x := range p.Models {
claudeOneHour(id, x.Cost)
OneHourFor(id, x.Cost)
if e := x.efforts(); len(e) > 0 {
l := strings.Join(e, ",")
if levels[bareID(id)] == nil {
+16 -4
View File
@@ -56,12 +56,24 @@ func TestPriceTiers(t *testing.T) {
}
// Anthropic bills a 1-hour cache write at 2× input and a 5-minute one at
// 1.25×: a call's 1-hour writes at theirs, the price's own else 2× input,
// and a call from before the split was kept (none for 1 hour) as before.
// 1.25×: a call's 1-hour writes at theirs, the price's own else the
// 5-minute one (PAMI on Discord: not 2× input for a model with no 1-hour
// writes at all), and a call from before the split was kept (none for 1
// hour) as before. A Claude model's price is given 2× input.
func TestCostSplit(t *testing.T) {
p := Price{Input: 5, Output: 25, CacheRead: 0.5, CacheWrite: 6.25}
if got, want := p.CostSplit(1000, 100, 0, 3000, 1000), (1000*5.0+100*25+2000*6.25+1000*10)/1e6; !near(got, want) {
t.Errorf("unset 1h: %v, want %v", got, want)
if got, want := p.CostSplit(1000, 100, 0, 3000, 1000), (1000*5.0+100*25+3000*6.25)/1e6; !near(got, want) {
t.Errorf("unset 1h: %v, want %v, every write at the 5-minute price", got, want)
}
c := p
OneHourFor("claude-sonnet-5-5", &c)
if got, want := c.CostSplit(1000, 100, 0, 3000, 1000), (1000*5.0+100*25+2000*6.25+1000*10)/1e6; !near(got, want) {
t.Errorf("claude unset 1h: %v, want %v", got, want)
}
g := p
OneHourFor("gpt-6-astra", &g)
if g.CacheWrite1h != 0 {
t.Errorf("gpt given a 1-hour write: %v", g.CacheWrite1h)
}
p.CacheWrite1h = 8
if got, want := p.CostSplit(1000, 100, 0, 3000, 1000), (1000*5.0+100*25+2000*6.25+1000*8)/1e6; !near(got, want) {
+7 -2
View File
@@ -8619,7 +8619,9 @@ function renderModels(p) {
// Usage page counts it (#819: only `magpie model price` set it):
// each box shows its list price until a price is given, a part left
// empty is the list's, and every part empty is its list price again
// the 1-hour cache write, empty, is 2× input as Anthropic bills it;
// the 1-hour cache write, empty, is 2× input as Anthropic bills it,
// and only a Claude model (or one priced with one) has the box: no
// other vendor has 1-hour writes (PAMI on Discord);
// a long-context price (OpenAI's over 272K) is what the whole request
// costs when its input is over its size, empty parts its list's
const parts = [["input", "Input"], ["output", "Output"], ["cache_read", "Cache read"], ["cache_write", "Cache write"], ["cache_write_1h", "Cache write 1h"]];
@@ -8647,7 +8649,10 @@ function renderModels(p) {
const value = typed ? typed[n] : !pr ? "" : k === "cache_write_1h" ? pr.cache_write_1h ? shown(pr.cache_write_1h) : "" : shown(pr[k]);
const placeholder = !m.list ? "" : k === "cache_write_1h" ? shown(oneHour(m.list)) : shown(m.list[k]);
const i = cell(value, placeholder, k, t(l));
if (k === "cache_write_1h") box.title = t("A cache write kept for an hour, as Anthropic bills it; empty: 2× input");
if (k === "cache_write_1h") {
box.title = t("A cache write kept for an hour, as Anthropic bills it; empty: 2× input");
box.hidden = !/claude/i.test(m.id) && !m.list?.cache_write_1h && !pr?.cache_write_1h && !(typed && typed[n]);
}
box.append(el("span", "", t(l)), i);
priceBox.append(box);
return i;
@@ -80,7 +80,11 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
assert.deepEqual(await tier("gpt-6-astra").evaluateAll((is) => is.map((i) => [i.value, i.placeholder])), [["", "272K"], ["", "20"], ["", "75"], ["", "2"], ["", "25"]]);
// the 1-hour box shows the list's, else 2× its input
assert.deepEqual(await base("claude-opus-5-5").evaluateAll((is) => is.map((i) => i.placeholder)), ["5", "25", "0.5", "6.25", "10"]);
assert.deepEqual(await base("gpt-6-astra").evaluateAll((is) => is.at(-1).placeholder), "20");
// only a Claude model has a 1-hour box: gpt-6-astra has no such write
// to price, so no 2× input one is shown for it (PAMI on Discord)
assert(await row("claude-opus-5-5").getByRole("textbox", { name: L.oneHour, exact: true }).isVisible());
assert(!(await row("gpt-6-astra").getByRole("textbox", { name: L.oneHour, exact: true }).isVisible()));
assert(!(await row("plain-1").getByRole("textbox", { name: L.oneHour, exact: true }).isVisible()));
// where there is none, the row waits behind a link
assert(!(await row("plain-1").locator(".mtier").isVisible()));
assert(await row("plain-1").locator(".mtier-add").isVisible());
+2
View File
@@ -917,6 +917,8 @@ func EffectivePriceIn(s settings.Settings, providerID, model string) (catalog.Pr
for _, key := range [...]string{id + "/" + model, id + "/*", AnyPriceKey(model)} {
if m, ok := s.ModelPrices[key]; ok {
if pr, bad := m.Price(); bad == "" {
// a Claude model's 1-hour cache write left out: 2× input
catalog.OneHourFor(model, &pr)
return pr, true
}
}
+1
View File
@@ -1248,6 +1248,7 @@ func priceOf(s settings.Settings, model string) (catalog.Price, bool) {
// from them
if p, ok := s.ModelPrices[provider.AnyPriceKey(bare)]; ok {
if pr, bad := p.Price(); bad == "" {
catalog.OneHourFor(bare, &pr)
return pr, true
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ func TestModelPriceTiers(t *testing.T) {
if err := json.Unmarshal([]byte(`{"input":1,"output":2,"cache_read":0.1,"cache_write":0}`), &old); err != nil {
t.Fatal(err)
}
if got, bad := old.Price(); bad != "" || !got.Same(catalog.Price{Input: 1, Output: 2, CacheRead: 0.1}) || got.OneHour() != 2 {
if got, bad := old.Price(); bad != "" || !got.Same(catalog.Price{Input: 1, Output: 2, CacheRead: 0.1}) || got.OneHour() != 0 {
t.Errorf("an old price = %+v, %q", got, bad)
}
if b, _ := json.Marshal(StatedPrice(catalog.Price{Input: 1, Output: 2})); strings.Contains(string(b), "cache_write_1h") || strings.Contains(string(b), "tiers") {
+2 -1
View File
@@ -374,7 +374,8 @@ type ModelPrice struct {
CacheRead *float64 `json:"cache_read,omitempty"`
CacheWrite *float64 `json:"cache_write,omitempty"`
// CacheWrite1h is a 1-hour cache write's price, absent where none is
// given: such a write is then counted at 2× input (catalog.Price).
// given: such a write is then counted at 2× input for a Claude model,
// else at the 5-minute price (catalog.OneHourFor).
CacheWrite1h *float64 `json:"cache_write_1h,omitempty"`
// Tiers are the prices of a request whose input is over a size, each
// whole: "magpie model price … --tier 272k …" (PAMI on Discord).
+7 -4
View File
@@ -27,7 +27,7 @@ const modelUsage = `usage:
say what it costs, in USD per million tokens, all four parts as
0.12,1.20,0.01,0.15; 0 is a model served for nothing, which is
a price, not the absence of one; a fifth part is a 1-hour cache
write's (Anthropic's 2× input, what it is counted at if not given)
write's (not given: a Claude model's is 2× input, any other's the 4th)
magpie model price <provider/model> <in>,<out>,<cache read>,<cache write> --tier 272k <in>,<out>,<cr>,<cw>
and what a request whose input, cached tokens included, is over
272K costs, the whole request; --tier may be given again
@@ -389,10 +389,12 @@ func parsePrice(rest []string) (catalog.Price, error) {
// priceDetail is the parts of a price after the two perMillion gives: its
// cache's, and what a request over each of its sizes costs.
// A 1-hour cache write is shown only where the price has one — a Claude
// model's, or one given — not one no vendor bills (PAMI on Discord).
func priceDetail(p catalog.Price) []string {
cache := " · cache read " + money(p.CacheRead) + ", cache write " + money(p.CacheWrite) + " (5 min), " + money(p.OneHour()) + " (1 hour)"
if p.CacheWrite1h <= 0 {
cache += " at 2× input"
cache := " · cache read " + money(p.CacheRead) + ", cache write " + money(p.CacheWrite)
if p.CacheWrite1h > 0 {
cache += " (5 min), " + money(p.CacheWrite1h) + " (1 hour)"
}
lines := []string{cache}
for _, t := range p.Tiers {
@@ -424,6 +426,7 @@ func anyModelPrice(ref string, rest []string) error {
}
if len(rest) == 0 {
if pr, ok := statedPrice(settings.Load().ModelPrices, key); ok {
catalog.OneHourFor(strings.TrimPrefix(key, settings.AnyProvider), &pr)
fmt.Println(bold.Render(perMillion(pr)), muted.Render("· "+key))
for _, l := range priceDetail(pr) {
fmt.Println(faint.Render(l))
+20
View File
@@ -37,6 +37,26 @@ func TestModelPriceTierCmd(t *testing.T) {
t.Errorf("magpie model prices: %q; want its tier", out)
}
// PAMI on Discord: four parts give no 1-hour cache write but a Claude
// model's — gpt-6-astra was shown "$20 (1 hour) at 2× input", a write
// OpenAI has no price for
if strings.Contains(out, "1 hour") || strings.Contains(out, "2× input") {
t.Errorf("magpie model price a/m: %q; want no 1-hour cache write", out)
}
if pr.OneHour() != 12.5 {
t.Errorf("a/m 1h = %v, want the 5-minute 12.5", pr.OneHour())
}
if err := modelCmd([]string{"price", "a/claude-opus-5-5", "3,15,0.3,3.75"}); err != nil {
t.Fatal(err)
}
if pr, _ := provider.EffectivePrice("a", "claude-opus-5-5"); pr.OneHour() != 6 {
t.Errorf("claude 1h = %+v, want 2× input", pr)
}
out, _ = said(t, func() error { return modelCmd([]string{"price", "a/claude-opus-5-5"}) })
if !strings.Contains(out, "$6.00 (1 hour)") && !strings.Contains(out, "$6 (1 hour)") {
t.Errorf("magpie model price a/claude-opus-5-5: %q; want its 1-hour cache write", out)
}
if err := modelCmd([]string{"price", "a/only-a", "5,25,0.5,6.25,8"}); err != nil {
t.Fatal(err)
}