Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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"`.
Expand Down
6 changes: 3 additions & 3 deletions internal/app/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/configcmd/configcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
8 changes: 4 additions & 4 deletions internal/cmd/initcmd/initcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ",
})
Expand Down Expand Up @@ -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)
}
}
Expand Down
20 changes: 10 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand All @@ -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,
},
Expand All @@ -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,
},
{
Expand All @@ -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,
Expand Down
22 changes: 11 additions & 11 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -446,19 +446,19 @@ 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",
},
},
{
name: "openai api",
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",
},
},
{
Expand All @@ -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{}},
Expand Down
2 changes: 1 addition & 1 deletion internal/pipeline/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) {
Expand Down
4 changes: 3 additions & 1 deletion internal/pricing/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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},
Expand All @@ -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},
}
Expand Down
30 changes: 30 additions & 0 deletions internal/pricing/pricing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/stagemodel/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/view/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading