From 8229e6c09c518943fb3fed70293035a6f9c90720 Mon Sep 17 00:00:00 2001 From: Aaron Wong <6979793+zzwong@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:50:54 -0400 Subject: [PATCH] fix(config): move built-in model defaults to Opus 5.5 and GPT-6 The built-in tier defaults were one to two releases behind. Claude CLI's large tier now uses claude-opus-5-5, which is cheaper than Opus 5. Codex CLI and the OpenAI API move from gpt-5.4-mini / gpt-5.4 / gpt-5.5 to gpt-6-luna / gpt-6-sol / gpt-6-sol, which cost less and avoid depending on GPT-5.5 staying available in Codex. Price claude-opus-5-5 at Anthropic's list rates, including its reduced 0.05x cache-read rate and fast-mode rates, so its cost is estimated instead of unavailable when an adapter reports none. Add Opus 5.5 and the GPT-6 models to the fast-mode lists; both providers list fast-mode pricing for them. --- README.md | 11 +++++---- internal/app/runtime_test.go | 6 ++--- internal/cmd/configcmd/configcmd_test.go | 2 +- internal/cmd/initcmd/initcmd_test.go | 8 +++---- internal/config/config.go | 20 ++++++++-------- internal/config/config_test.go | 22 ++++++++--------- internal/pipeline/pipeline_test.go | 2 +- internal/pricing/pricing.go | 4 +++- internal/pricing/pricing_test.go | 30 ++++++++++++++++++++++++ internal/stagemodel/resolver_test.go | 4 ++-- internal/view/config_test.go | 2 +- 11 files changed, 72 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 73b7df9..422648e 100644 --- a/README.md +++ b/README.md @@ -761,9 +761,9 @@ Built-in model maps: | Provider | Adapter | small | medium | large | |----------|---------|-------|--------|-------| -| `openai` | `codex_cli` | `gpt-5.4-mini` | `gpt-5.4` | `gpt-5.5` | -| `openai` | `openai_api` | `gpt-5.4-mini` | `gpt-5.4` | `gpt-5.5` | -| `anthropic` | `claude_cli` | `claude-haiku-4-5` | `claude-sonnet-5` | `claude-opus-5` | +| `openai` | `codex_cli` | `gpt-6-luna` | `gpt-6-sol` | `gpt-6-sol` | +| `openai` | `openai_api` | `gpt-6-luna` | `gpt-6-sol` | `gpt-6-sol` | +| `anthropic` | `claude_cli` | `claude-haiku-4-5` | `claude-sonnet-5` | `claude-opus-5-5` | | `anthropic` | `anthropic_api` | unset | unset | unset | | `pi` | `pi_rpc` | unset | unset | unset | @@ -1247,8 +1247,9 @@ Policy and output flags: Fast mode defaults off; set `fast: true` on a profile to enable it by default. `--fast` and `--no-fast` override the profile. Unsupported runtime/model combinations warn and continue at normal speed. Fast mode supports `claude_cli` -and `anthropic_api` with `claude-opus-5` or `claude-opus-4-8`, and `codex_cli` -with `gpt-5.4`, `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, or `gpt-5.6-luna`. +and `anthropic_api` with `claude-opus-5-5`, `claude-opus-5`, or `claude-opus-4-8`, +and `codex_cli` with `gpt-5.4`, `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, +`gpt-5.6-luna`, `gpt-6-astra`, `gpt-6-sol`, or `gpt-6-luna`. Anthropic has removed Opus 4.7 fast mode. Claude CLI receives a per-session `fastMode` setting, Anthropic API requests use its fast-mode beta, and Codex CLI receives `service_tier="fast"`. diff --git a/internal/app/runtime_test.go b/internal/app/runtime_test.go index 0df481a..956ee08 100644 --- a/internal/app/runtime_test.go +++ b/internal/app/runtime_test.go @@ -950,9 +950,9 @@ func TestNewAdapterCreatesSupportedCLIAdapters(t *testing.T) { func TestAdapterConstructorsCoverLLMRuntimeSpecs(t *testing.T) { want := make(map[config.LLMAdapter]struct{}, len(config.LLMRuntimeSpecs())) wantFastModels := map[config.LLMAdapter][]string{ - config.LLMAdapterClaudeCLI: {"claude-opus-5", "claude-opus-4-8"}, - config.LLMAdapterAnthropicAPI: {"claude-opus-5", "claude-opus-4-8"}, - config.LLMAdapterCodexCLI: {"gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"}, + config.LLMAdapterClaudeCLI: {"claude-opus-5-5", "claude-opus-5", "claude-opus-4-8"}, + config.LLMAdapterAnthropicAPI: {"claude-opus-5-5", "claude-opus-5", "claude-opus-4-8"}, + config.LLMAdapterCodexCLI: {"gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna"}, } for _, spec := range config.LLMRuntimeSpecs() { if _, duplicate := want[spec.Adapter]; duplicate { diff --git a/internal/cmd/configcmd/configcmd_test.go b/internal/cmd/configcmd/configcmd_test.go index 3b19d4b..8e66d82 100644 --- a/internal/cmd/configcmd/configcmd_test.go +++ b/internal/cmd/configcmd/configcmd_test.go @@ -2132,7 +2132,7 @@ func TestConfigLLMModelsListAndResolve(t *testing.T) { } if !strings.Contains(out.String(), "small: claude-haiku-4-5 (built_in)") || !strings.Contains(out.String(), "medium: claude-sonnet-5 (built_in)") || - !strings.Contains(out.String(), "large: claude-opus-5 (built_in)") { + !strings.Contains(out.String(), "large: claude-opus-5-5 (built_in)") { t.Fatalf("list stdout = %q, want effective Claude CLI defaults", out.String()) } diff --git a/internal/cmd/initcmd/initcmd_test.go b/internal/cmd/initcmd/initcmd_test.go index b028302..9dcef58 100644 --- a/internal/cmd/initcmd/initcmd_test.go +++ b/internal/cmd/initcmd/initcmd_test.go @@ -6856,7 +6856,7 @@ func TestNormalizeInitModelMapDropsBuiltInsAndBlanks(t *testing.T) { Adapter: config.LLMAdapterCodexCLI, } got := normalizeInitModelMap(llm, config.ModelMap{ - "small": "gpt-5.4-mini", + "small": "gpt-6-luna", "medium": " custom-medium ", "large": " \t ", }) @@ -10265,14 +10265,14 @@ func TestInitProfileV2LLMRuntimeSelectionRefreshesModelMapFields(t *testing.T) { model = selectInitProfileV2FieldValue(t, model, initProfileV2FieldLLMRuntime, "openai-work") - if got := model.document.fieldValue(initProfileV2FieldModelMap(config.ModelTierSmall)); got != "gpt-5.4-mini" { + if got := model.document.fieldValue(initProfileV2FieldModelMap(config.ModelTierSmall)); got != "gpt-6-luna" { t.Fatalf("small model after runtime change = %q, want OpenAI built-in", got) } - if got := model.document.fieldValue(initProfileV2FieldModelMap(config.ModelTierMedium)); got != "gpt-5.4" { + if got := model.document.fieldValue(initProfileV2FieldModelMap(config.ModelTierMedium)); got != "gpt-6-sol" { t.Fatalf("medium model after runtime change = %q, want OpenAI built-in", got) } smallIndex := model.document.fieldIndexByID(initProfileV2FieldModelMap(config.ModelTierSmall)) - if smallIndex < 0 || !strings.Contains(model.document[smallIndex].Description, "Built-in small model for this runtime: gpt-5.4-mini.") { + if smallIndex < 0 || !strings.Contains(model.document[smallIndex].Description, "Built-in small model for this runtime: gpt-6-luna.") { t.Fatalf("small model description after runtime change = %q", model.document[smallIndex].Description) } } diff --git a/internal/config/config.go b/internal/config/config.go index c054eb0..0694683 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -553,9 +553,9 @@ var llmRuntimeSpecs = []LLMRuntimeSpec{ BuiltInModelMap: ModelMap{ string(ModelTierSmall): "claude-haiku-4-5", string(ModelTierMedium): "claude-sonnet-5", - string(ModelTierLarge): "claude-opus-5", + string(ModelTierLarge): "claude-opus-5-5", }, - FastModeModels: []string{"claude-opus-5", "claude-opus-4-8"}, + FastModeModels: []string{"claude-opus-5-5", "claude-opus-5", "claude-opus-4-8"}, MaximumEffort: modelprefs.EffortHigh, }, { @@ -565,7 +565,7 @@ var llmRuntimeSpecs = []LLMRuntimeSpec{ SuggestedName: "anthropic-api-key", DisplayName: "Anthropic API", BuiltInModelMap: ModelMap{}, - FastModeModels: []string{"claude-opus-5", "claude-opus-4-8"}, + FastModeModels: []string{"claude-opus-5-5", "claude-opus-5", "claude-opus-4-8"}, MaximumEffort: modelprefs.EffortHigh, RequiresCredentialRef: true, }, @@ -576,11 +576,11 @@ var llmRuntimeSpecs = []LLMRuntimeSpec{ SuggestedName: "codex-cli", DisplayName: "Codex CLI", BuiltInModelMap: ModelMap{ - string(ModelTierSmall): "gpt-5.4-mini", - string(ModelTierMedium): "gpt-5.4", - string(ModelTierLarge): "gpt-5.5", + string(ModelTierSmall): "gpt-6-luna", + string(ModelTierMedium): "gpt-6-sol", + string(ModelTierLarge): "gpt-6-sol", }, - FastModeModels: []string{"gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"}, + FastModeModels: []string{"gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna"}, MaximumEffort: modelprefs.EffortHigh, }, { @@ -590,9 +590,9 @@ var llmRuntimeSpecs = []LLMRuntimeSpec{ SuggestedName: "openai-api-key", DisplayName: "OpenAI API", BuiltInModelMap: ModelMap{ - string(ModelTierSmall): "gpt-5.4-mini", - string(ModelTierMedium): "gpt-5.4", - string(ModelTierLarge): "gpt-5.5", + string(ModelTierSmall): "gpt-6-luna", + string(ModelTierMedium): "gpt-6-sol", + string(ModelTierLarge): "gpt-6-sol", }, RequiresCredentialRef: true, MaximumEffort: modelprefs.EffortHigh, diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1cb7bec..c4e6d58 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -387,14 +387,14 @@ func TestModelMapValidationAndResolution(t *testing.T) { t.Fatalf("ResolveProfile: %v", err) } effective := EffectiveModelMap(resolved.LLM) - if effective[ModelTierSmall].Model != "gpt-5.4-mini" || effective[ModelTierSmall].Source != ModelMapSourceBuiltIn { - t.Fatalf("small resolution = %#v, want built-in gpt-5.4-mini", effective[ModelTierSmall]) + if effective[ModelTierSmall].Model != "gpt-6-luna" || effective[ModelTierSmall].Source != ModelMapSourceBuiltIn { + t.Fatalf("small resolution = %#v, want built-in gpt-6-luna", effective[ModelTierSmall]) } if effective[ModelTierMedium].Model != "gpt-custom" || effective[ModelTierMedium].Source != ModelMapSourceConfig { t.Fatalf("medium resolution = %#v, want config override", effective[ModelTierMedium]) } - if got, ok := ResolveModelTier(resolved.LLM, ModelTierLarge); !ok || got.Model != "gpt-5.5" || got.Source != ModelMapSourceBuiltIn { - t.Fatalf("ResolveModelTier large = %#v ok=%t, want built-in gpt-5.5", got, ok) + if got, ok := ResolveModelTier(resolved.LLM, ModelTierLarge); !ok || got.Model != "gpt-6-sol" || got.Source != ModelMapSourceBuiltIn { + t.Fatalf("ResolveModelTier large = %#v ok=%t, want built-in gpt-6-sol", got, ok) } if resolved.LLM.ReviewerModelTier != "" { t.Fatalf("ReviewerModelTier = %q, want empty by default", resolved.LLM.ReviewerModelTier) @@ -446,9 +446,9 @@ func TestBuiltInModelMapIsProviderAdapterSpecific(t *testing.T) { provider: LLMProviderOpenAI, adapter: LLMAdapterCodexCLI, want: ModelMap{ - "small": "gpt-5.4-mini", - "medium": "gpt-5.4", - "large": "gpt-5.5", + "small": "gpt-6-luna", + "medium": "gpt-6-sol", + "large": "gpt-6-sol", }, }, { @@ -456,9 +456,9 @@ func TestBuiltInModelMapIsProviderAdapterSpecific(t *testing.T) { provider: LLMProviderOpenAI, adapter: LLMAdapterOpenAIAPI, want: ModelMap{ - "small": "gpt-5.4-mini", - "medium": "gpt-5.4", - "large": "gpt-5.5", + "small": "gpt-6-luna", + "medium": "gpt-6-sol", + "large": "gpt-6-sol", }, }, { @@ -468,7 +468,7 @@ func TestBuiltInModelMapIsProviderAdapterSpecific(t *testing.T) { want: ModelMap{ "small": "claude-haiku-4-5", "medium": "claude-sonnet-5", - "large": "claude-opus-5", + "large": "claude-opus-5-5", }, }, {name: "anthropic api", provider: LLMProviderAnthropic, adapter: LLMAdapterAnthropicAPI, want: ModelMap{}}, diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index b7fb39b..af43d65 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -3760,7 +3760,7 @@ func TestDryRunReviewerFloorsResolveIndependentlyPerAgent(t *testing.T) { FloorTier: "large", BaselineTier: "small", EffectiveTier: "large", - ResolvedModel: "claude-opus-5", + ResolvedModel: "claude-opus-5-5", ResolvedEffort: "medium", ModelMapSource: config.ModelMapSourceBuiltIn, }) { diff --git a/internal/pricing/pricing.go b/internal/pricing/pricing.go index 3a63df6..0b7c3c4 100644 --- a/internal/pricing/pricing.go +++ b/internal/pricing/pricing.go @@ -9,7 +9,7 @@ package pricing // TableVersion identifies the public list-price snapshot used by estimates. -const TableVersion = "anthropic-public-2026-09-02" +const TableVersion = "anthropic-public-2026-09-23" // rate is the USD list price per 1,000,000 tokens for each billable category. type rate struct { @@ -24,6 +24,7 @@ type rate struct { // models; absent models simply return no estimate. var rates = map[string]rate{ "claude-fable-5-1": {in: 10, out: 50, cacheRead: 0.25, cacheWrite5: 12.5, cacheWrite1: 20}, + "claude-opus-5-5": {in: 4, out: 20, cacheRead: 0.2, cacheWrite5: 5, cacheWrite1: 8}, "claude-opus-5": {in: 5, out: 25, cacheRead: 0.5, cacheWrite5: 6.25, cacheWrite1: 10}, "claude-opus-4-8": {in: 5, out: 25, cacheRead: 0.5, cacheWrite5: 6.25, cacheWrite1: 10}, "claude-sonnet-5": {in: 2, out: 10, cacheRead: 0.2, cacheWrite5: 2.5, cacheWrite1: 4}, @@ -33,6 +34,7 @@ var rates = map[string]rate{ } var fastRates = map[string]rate{ + "claude-opus-5-5": {in: 8, out: 40, cacheRead: 0.4, cacheWrite5: 10, cacheWrite1: 16}, "claude-opus-5": {in: 10, out: 50, cacheRead: 1, cacheWrite5: 12.5, cacheWrite1: 20}, "claude-opus-4-8": {in: 10, out: 50, cacheRead: 1, cacheWrite5: 12.5, cacheWrite1: 20}, } diff --git a/internal/pricing/pricing_test.go b/internal/pricing/pricing_test.go index 03fd170..aaed04f 100644 --- a/internal/pricing/pricing_test.go +++ b/internal/pricing/pricing_test.go @@ -112,6 +112,36 @@ func TestEstimateUsageUSDPricesCacheTTLAndObservedSpeed(t *testing.T) { } } +func TestEstimateUsageUSDPricesOpus55ReducedCacheReadAndFastMode(t *testing.T) { + // Opus 5.5 reads cache at 0.05x input, not the usual 0.1x, and fast mode + // doubles every bucket: 4 + 20 + 0.20 + 5 + 8 = 37.2 standard. + usage := Usage{ + TokensIn: p(1_000_000), + TokensOut: p(1_000_000), + CacheRead: p(1_000_000), + CacheCreate5m: p(1_000_000), + CacheCreate1h: p(1_000_000), + Speed: "standard", + } + + standard, ok := EstimateUsageUSD("claude-opus-5-5", usage) + if !ok { + t.Fatal("expected standard Opus 5.5 usage to be priced") + } + if want := 37.2; math.Abs(standard-want) > 1e-9 { + t.Fatalf("standard cost = %v, want %v", standard, want) + } + + usage.Speed = "fast" + fast, ok := EstimateUsageUSD("claude-opus-5-5", usage) + if !ok { + t.Fatal("expected fast Opus 5.5 usage to be priced") + } + if want := 74.4; math.Abs(fast-want) > 1e-9 { + t.Fatalf("fast cost = %v, want %v", fast, want) + } +} + func TestEstimateUsageUSDPricesEachSonnetBucketIndependently(t *testing.T) { tests := []struct { name string diff --git a/internal/stagemodel/resolver_test.go b/internal/stagemodel/resolver_test.go index 4b90343..f075151 100644 --- a/internal/stagemodel/resolver_test.go +++ b/internal/stagemodel/resolver_test.go @@ -62,8 +62,8 @@ func TestResolveStageModelAppliesEffortOverrideWithoutBypassingTier(t *testing.T if err != nil { t.Fatalf("ResolveStageModel: %v", err) } - if got.Model != "gpt-5.4" { - t.Fatalf("Model = %q, want gpt-5.4", got.Model) + if got.Model != "gpt-6-sol" { + t.Fatalf("Model = %q, want gpt-6-sol", got.Model) } if got.Effort != "high" { t.Fatalf("Effort = %q, want high", got.Effort) diff --git a/internal/view/config_test.go b/internal/view/config_test.go index 2d3def2..2fa2d64 100644 --- a/internal/view/config_test.go +++ b/internal/view/config_test.go @@ -213,7 +213,7 @@ LLM: Model map: small: claude-haiku-4-5 (built_in) medium: claude-sonnet-5 (built_in) [max effort: low] - large: claude-opus-5 (built_in) + large: claude-opus-5-5 (built_in) Credentials: - git: codereview/home (pat) git_token: missing