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:
1 parent
a532d1586c
commit
3a30ac87c2
11 files changed
+77
-24
No files matched your search
+4
-2
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user