From 4b26d9fbaf7f9838e81456098caeedec3dbb7de1 Mon Sep 17 00:00:00 2001 From: Patrick Linnane Date: Sat, 12 Sep 2026 10:13:20 -0700 Subject: [PATCH] fix(pricing): correct gpt-5.6-sol rates and add the daybreak blue alias The gpt-5.6-sol row carried 2.00 in and 10.00 out per million tokens, with cache reads at 0.20 and writes at 2.50. Those are OpenAI's Batch and Flex figures; the Standard tier is 4.00, 20.00, 0.40, and 5.00, so Sol usage was estimated at half its list cost. gpt-daybreak-blue-latest, which OpenAI documents as an approval-gated alias of Sol, had no row and priced at zero. The row now carries the Standard rates, annotated with the promotional window OpenAI publishes so the figures get rechecked when it lapses. CostFromUsage resolves the Daybreak alias onto Sol after normalizing the ID, so the OpenCode provider prefix and context suffix still strip, and a table-driven test pins each rate and the cache arithmetic for both IDs in their bare and prefixed forms. Signed-off-by: Patrick Linnane --- copilot.go | 2 +- harness.go | 2 ++ pricing.go | 14 +++++++++++--- pricing_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 pricing_test.go diff --git a/copilot.go b/copilot.go index 4d60d72..5e6f44a 100644 --- a/copilot.go +++ b/copilot.go @@ -558,7 +558,7 @@ func (CopilotHarness) DefaultModels() []ModelDefault { // entry as the default. Dotted Anthropic IDs are distinct from Claude Code's // hyphenated IDs. return []ModelDefault{ - {Name: "GPT-5.6 Sol", ID: "gpt-5.6-sol"}, + {Name: "GPT-5.6 Sol", ID: modelGPT56SolID}, {Name: "Claude Sonnet 5", ID: modelClaudeSonnet5ID}, {Name: "Claude Opus 5", ID: modelClaudeOpus5ID, Tier: modelTierMax}, {Name: "Claude Opus 4.8", ID: "claude-opus-4.8"}, diff --git a/harness.go b/harness.go index ca2b1a0..267fbf0 100644 --- a/harness.go +++ b/harness.go @@ -18,6 +18,8 @@ const ( modelGPT54MiniID = "gpt-5.4-mini" modelGPT54ID = "gpt-5.4" modelGPT55ID = "gpt-5.5" + modelGPT56SolID = "gpt-5.6-sol" + modelDaybreakBlueID = "gpt-daybreak-blue-latest" modelTierMid = "mid" modelTierHigh = "high" diff --git a/pricing.go b/pricing.go index 34fe03d..accfd66 100644 --- a/pricing.go +++ b/pricing.go @@ -12,7 +12,7 @@ type modelPrice struct { // no dollar amount. Rows for backends that report cost directly remain here so // tests can require every default model to have a known price. // -// Prices are USD rates per million tokens as of 2026-08. Copilot-only rows use +// Prices are USD rates per million tokens. Copilot-only rows use // the token prices returned by CLI 1.0.80's models.list, converting one AI // credit to $0.01. Update this table with each backend's DefaultModels list. // @@ -38,7 +38,9 @@ var modelPricing = map[string]modelPrice{ "claude-haiku-4.5": {In: 1.00, Out: 5.00, CachedIn: 0.10, CacheWrite: 1.25}, // OpenAI - "gpt-5.6-sol": {In: 2.00, Out: 10.00, CachedIn: 0.20, CacheWrite: 2.50}, + // Sol's promotional standard rates apply at least through 2026-11-21; recheck then. + // https://developers.openai.com/api/docs/pricing + modelGPT56SolID: {In: 4.00, Out: 20.00, CachedIn: 0.40, CacheWrite: 5.00}, "gpt-5.6-terra": {In: 2.00, Out: 12.00, CachedIn: 0.20, CacheWrite: 2.50}, "gpt-5.6-luna": {In: 0.20, Out: 1.20, CachedIn: 0.02, CacheWrite: 0.25}, modelGPT55ID: {In: 5.00, Out: 30.00, CachedIn: 0.50}, @@ -68,7 +70,13 @@ const perMillion = 1e6 // subset. CacheWriteTokens is separate only for models with a dedicated write // rate; it remains ordinary input when CacheWrite is zero. func CostFromUsage(model string, usage Usage) float64 { - price, ok := modelPricing[normalizeModelID(model)] + model = normalizeModelID(model) + // Daybreak Blue currently aliases Sol and shares its pricing. + // https://developers.openai.com/api/docs/models/gpt-daybreak-blue-latest + if model == modelDaybreakBlueID { + model = modelGPT56SolID + } + price, ok := modelPricing[model] if !ok { return 0 } diff --git a/pricing_test.go b/pricing_test.go new file mode 100644 index 0000000..c66abab --- /dev/null +++ b/pricing_test.go @@ -0,0 +1,39 @@ +package harness + +import ( + "math" + "testing" +) + +func TestCostFromUsage_gpt56SolAndDaybreakBasePricing(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + usage Usage + want float64 + }{ + {"uncached input", Usage{InputTokens: 100_000}, 0.40}, + {"output", Usage{OutputTokens: 10_000}, 0.20}, + {"cache reads", Usage{InputTokens: 100_000, CacheReadTokens: 100_000}, 0.04}, + {"cache writes", Usage{InputTokens: 100_000, CacheWriteTokens: 100_000}, 0.50}, + {"standard scan", Usage{InputTokens: 100_000, OutputTokens: 10_000}, 0.60}, + {"mixed", Usage{InputTokens: 100_000, OutputTokens: 10_000, CacheReadTokens: 10_000, CacheWriteTokens: 20_000}, 0.584}, + } + for _, model := range []struct{ name, id string }{ + {"sol", "gpt-5.6-sol"}, + {"sol_normalized", "openai/gpt-5.6-sol[1m]"}, + {"daybreak", "gpt-daybreak-blue-latest"}, + {"daybreak_normalized", "openai/gpt-daybreak-blue-latest[1m]"}, + } { + t.Run(model.name, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CostFromUsage(model.id, tt.usage); math.Abs(got-tt.want) > 1e-9 { + t.Errorf("CostFromUsage(%q, %+v) = %v, want %v", model.id, tt.usage, got, tt.want) + } + }) + } + }) + } +}