From 7a3a50ea7adc1b0ed3435e7f4555f1178318a9fb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 19:21:20 +0000 Subject: [PATCH 1/3] refactor: make internal/models the single source of truth for models Move pricing, model IDs, aliases and defaults out of internal/benchmark and internal/tokens into a new internal/models package. ResolveModel, flag help, and the unknown-model error are now derived from one registry, and DefaultCountTokensModel references it instead of duplicating an ID string. No behavior change. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HX8KccseqQb2rUserM2BDT --- cmd/cc-session/benchmark.go | 7 +- cmd/cc-session/benchmark_test.go | 49 +----------- internal/benchmark/cost_test.go | 50 ++++++------ internal/benchmark/model.go | 70 +---------------- internal/benchmark/model_test.go | 131 ------------------------------- internal/models/models.go | 112 ++++++++++++++++++++++++++ internal/models/models_test.go | 98 +++++++++++++++++++++++ internal/tokens/tokens.go | 4 +- 8 files changed, 252 insertions(+), 269 deletions(-) delete mode 100644 internal/benchmark/model_test.go create mode 100644 internal/models/models.go create mode 100644 internal/models/models_test.go diff --git a/cmd/cc-session/benchmark.go b/cmd/cc-session/benchmark.go index 9ccb045..5e3f8f2 100644 --- a/cmd/cc-session/benchmark.go +++ b/cmd/cc-session/benchmark.go @@ -12,6 +12,7 @@ import ( "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/analyzer" bm "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/benchmark" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/inject" + "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/parser" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/session" ) @@ -30,7 +31,7 @@ func runBenchmark(args []string, out io.Writer, errOut io.Writer, store parser.S days := fs.Int("days", 30, "how far back to scan") minKB := fs.Int("min-kb", 100, "minimum JSONL file size in KB") maxN := fs.Int("n", 10, "max sessions to include") - model := fs.String("model", "opus", "model: opus (opus-5-5), opus-4-6, opus-4-7, opus-4-8, sonnet (sonnet-5-5), sonnet-4-6, or fable (fable-5-1)") + model := fs.String("model", models.DefaultBenchmark.Alias(), "model: "+models.Usage()) overhead := fs.Int("overhead", 0, "session overhead tokens (system+tools+CLAUDE.md); measure with a 1-turn session") isNoAPI := fs.Bool("no-api", false, "skip API calls; estimate filtered-text tokens with chars/2 (offline fallback)") if err := fs.Parse(reorderArgs(args)); err != nil { @@ -47,12 +48,12 @@ func runBenchmark(args []string, out io.Writer, errOut io.Writer, store parser.S overheadToks = bm.DefaultOverhead } - modelConfig, err := bm.ResolveModel(*model) + modelConfig, err := models.Resolve(*model) if err != nil { return err } p := modelConfig.Pricing - tokenCountModel := modelConfig.TokenCountModel + tokenCountModel := modelConfig.ID all, _ := store.ListAllSessions() diff --git a/cmd/cc-session/benchmark_test.go b/cmd/cc-session/benchmark_test.go index 938b61a..4857479 100644 --- a/cmd/cc-session/benchmark_test.go +++ b/cmd/cc-session/benchmark_test.go @@ -11,6 +11,7 @@ import ( "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/analyzer" bm "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/benchmark" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/inject" + "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/parser" ) @@ -75,50 +76,6 @@ func TestPrintCompressionSection_GivenEvenCount_ThenPrintsAveragedMedian(t *test } } -func TestResolveBenchmarkModel_GivenAcceptedModelNames_ThenReturnsPricingAndTokenCountingModel(t *testing.T) { - tests := []struct { - name string - wantPricing bm.Pricing - wantTokenCountModel string - }{ - {name: "opus", wantPricing: bm.PricingOpus55, wantTokenCountModel: bm.TokenCountModelOpus55}, - {name: "opus-4-6", wantPricing: bm.PricingOpus, wantTokenCountModel: bm.TokenCountModelOpus46}, - {name: "opus-4-7", wantPricing: bm.PricingOpus, wantTokenCountModel: bm.TokenCountModelOpus47}, - {name: "opus-4-8", wantPricing: bm.PricingOpus, wantTokenCountModel: bm.TokenCountModelOpus48}, - {name: "sonnet", wantPricing: bm.PricingSonnet55, wantTokenCountModel: bm.TokenCountModelSonnet55}, - {name: "sonnet-4-6", wantPricing: bm.PricingSonnet46, wantTokenCountModel: bm.TokenCountModelSonnet46}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := bm.ResolveModel(tt.name) - if err != nil { - t.Fatalf("ResolveModel(%q) returned error: %v", tt.name, err) - } - if got.Pricing != tt.wantPricing { - t.Fatalf("pricing = %+v, want %+v", got.Pricing, tt.wantPricing) - } - if got.TokenCountModel != tt.wantTokenCountModel { - t.Fatalf("token count model = %q, want %q", got.TokenCountModel, tt.wantTokenCountModel) - } - }) - } -} - -func TestResolveBenchmarkModel_GivenUnknownModel_ThenReturnsAcceptedNames(t *testing.T) { - _, err := bm.ResolveModel("opus-4-5") - if err == nil { - t.Fatal("ResolveModel returned nil error for unknown model") - } - - got := err.Error() - for _, want := range []string{"opus", "opus-4-6", "opus-4-7", "opus-4-8", "sonnet", "sonnet-4-6"} { - if !strings.Contains(got, want) { - t.Fatalf("error message missing accepted model %q: %s", want, got) - } - } -} - func TestRunBenchmark_WhenSessionHasAPIUsage_ThenUsesTokenCountingAPIForNewContext(t *testing.T) { root := t.TempDir() sid := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" @@ -172,7 +129,7 @@ func TestRunBenchmark_WhenSessionHasAPIUsage_ThenUsesTokenCountingAPIForNewConte if countedText == "" { t.Fatal("countTokensFn was not called") } - wantCountModel := bm.TokenCountModelOpus55 + wantCountModel := models.Opus55.ID if countModel != wantCountModel { t.Fatalf("token counter model = %q, want %q", countModel, wantCountModel) } @@ -329,7 +286,7 @@ func TestRunBenchmark_GivenSonnetModel_ThenUsesSonnetTokenCounterModel(t *testin t.Fatalf("runBenchmark returned error: %v", err) } - wantCountModel := bm.TokenCountModelSonnet55 + wantCountModel := models.Sonnet55.ID if countModel != wantCountModel { t.Fatalf("token counter model = %q, want %q", countModel, wantCountModel) } diff --git a/internal/benchmark/cost_test.go b/internal/benchmark/cost_test.go index 94e9e78..9f72b8b 100644 --- a/internal/benchmark/cost_test.go +++ b/internal/benchmark/cost_test.go @@ -3,10 +3,16 @@ package benchmark import ( "math" "testing" + + "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" ) const epsilon = 1e-9 +// testPricing is the Opus 4.x price list; expected values below assume +// $5 base input, $6.25 cache write, $0.50 cache read. +var testPricing = models.Opus48.Pricing + func approxEqual(got, want float64) bool { return math.Abs(got-want) < epsilon } @@ -79,17 +85,17 @@ func Test_CumulativeCosts_GivenFractionalK_ThenUseFractionalCallsPerTurn(t *test }{ { name: "ColdExistingSession", - got: CumulativeCostA(1, 100000, testParamsK4478, PricingOpus), + got: CumulativeCostA(1, 100000, testParamsK4478, testPricing), want: 0.9506535, }, { name: "WarmExistingSession", - got: CumulativeCostAWarm(1, 100000, testParamsK4478, PricingOpus), + got: CumulativeCostAWarm(1, 100000, testParamsK4478, testPricing), want: 0.3916315, }, { name: "NewSession", - got: CumulativeCostB(1, 100000, 50000, testParamsK4478, PricingOpus), + got: CumulativeCostB(1, 100000, 50000, testParamsK4478, testPricing), want: 0.9157635, }, } @@ -164,7 +170,7 @@ var costATests = []struct { func Test_CumulativeCostA(t *testing.T) { for _, tc := range costATests { t.Run(tc.name, func(t *testing.T) { - got := CumulativeCostA(tc.turns, tc.x, tc.sp, PricingOpus) + got := CumulativeCostA(tc.turns, tc.x, tc.sp, testPricing) if !approxEqual(got, tc.want) { t.Errorf("CumulativeCostA(%d, %d) = %.10f, want %.10f", tc.turns, tc.x, got, tc.want) } @@ -225,7 +231,7 @@ var costAWarmTests = []struct { func Test_CumulativeCostAWarm(t *testing.T) { for _, tc := range costAWarmTests { t.Run(tc.name, func(t *testing.T) { - got := CumulativeCostAWarm(tc.turns, tc.x, tc.sp, PricingOpus) + got := CumulativeCostAWarm(tc.turns, tc.x, tc.sp, testPricing) if !approxEqual(got, tc.want) { t.Errorf("CumulativeCostAWarm(%d, %d) = %.10f, want %.10f", tc.turns, tc.x, got, tc.want) } @@ -293,7 +299,7 @@ var costBTests = []struct { func Test_CumulativeCostB(t *testing.T) { for _, tc := range costBTests { t.Run(tc.name, func(t *testing.T) { - got := CumulativeCostB(tc.turns, tc.x, tc.filteredTokens, tc.sp, PricingOpus) + got := CumulativeCostB(tc.turns, tc.x, tc.filteredTokens, tc.sp, testPricing) if !approxEqual(got, tc.want) { t.Errorf("CumulativeCostB(%d, %d, %d) = %.10f, want %.10f", tc.turns, tc.x, tc.filteredTokens, got, tc.want) } @@ -302,8 +308,8 @@ func Test_CumulativeCostB(t *testing.T) { } func Test_CumulativeCostBWithInjectPages_GivenMultiplePages_ThenSetupCostsMoreThanOneShot(t *testing.T) { - oneShot := CumulativeCostBWithInjectPages(0, 50000, 1, testParams, PricingOpus) - multiPage := CumulativeCostBWithInjectPages(0, 50000, 5, testParams, PricingOpus) + oneShot := CumulativeCostBWithInjectPages(0, 50000, 1, testParams, testPricing) + multiPage := CumulativeCostBWithInjectPages(0, 50000, 5, testParams, testPricing) if !approxEqual(oneShot, 0.5625) { t.Fatalf("one-page setup cost = %.10f, want historical one-shot cost 0.5625", oneShot) @@ -319,8 +325,8 @@ func Test_CumulativeCostBWithInjectPages_GivenMultiplePages_ThenSetupCostsMoreTh func Test_CumulativeCostAWarm_GivenAnyTurns_AlwaysCheaperThanColdCostA(t *testing.T) { x := 100000 for turns := 1; turns <= 20; turns++ { - cold := CumulativeCostA(turns, x, testParams, PricingOpus) - warm := CumulativeCostAWarm(turns, x, testParams, PricingOpus) + cold := CumulativeCostA(turns, x, testParams, testPricing) + warm := CumulativeCostAWarm(turns, x, testParams, testPricing) if warm >= cold { t.Errorf("turns=%d: warm (%.10f) >= cold (%.10f); warm should always be cheaper", turns, warm, cold) } @@ -329,9 +335,9 @@ func Test_CumulativeCostAWarm_GivenAnyTurns_AlwaysCheaperThanColdCostA(t *testin func Test_CumulativeCostA_GivenIncreasingTurns_ThenCostMonotonicallyIncreases(t *testing.T) { x := 100000 - prev := CumulativeCostA(1, x, testParams, PricingOpus) + prev := CumulativeCostA(1, x, testParams, testPricing) for turns := 2; turns <= 20; turns++ { - curr := CumulativeCostA(turns, x, testParams, PricingOpus) + curr := CumulativeCostA(turns, x, testParams, testPricing) if curr <= prev { t.Errorf("turns=%d: cost (%.10f) not greater than turns=%d cost (%.10f)", turns, curr, turns-1, prev) } @@ -341,9 +347,9 @@ func Test_CumulativeCostA_GivenIncreasingTurns_ThenCostMonotonicallyIncreases(t func Test_CumulativeCostAWarm_GivenIncreasingTurns_ThenCostMonotonicallyIncreases(t *testing.T) { x := 100000 - prev := CumulativeCostAWarm(1, x, testParams, PricingOpus) + prev := CumulativeCostAWarm(1, x, testParams, testPricing) for turns := 2; turns <= 20; turns++ { - curr := CumulativeCostAWarm(turns, x, testParams, PricingOpus) + curr := CumulativeCostAWarm(turns, x, testParams, testPricing) if curr <= prev { t.Errorf("turns=%d: cost (%.10f) not greater than turns=%d cost (%.10f)", turns, curr, turns-1, prev) } @@ -356,8 +362,8 @@ func Test_CumulativeCostB_GivenSufficientTurns_ThenCheaperThanCostA(t *testing.T filteredTokens := 50000 // base=90000 < x=100000 means compression happened found := false for turns := 1; turns <= 200; turns++ { - costA := CumulativeCostA(turns, x, testParams, PricingOpus) - costB := CumulativeCostB(turns, x, filteredTokens, testParams, PricingOpus) + costA := CumulativeCostA(turns, x, testParams, testPricing) + costB := CumulativeCostB(turns, x, filteredTokens, testParams, testPricing) if costB < costA { found = true break @@ -372,8 +378,8 @@ func Test_CumulativeCostB_GivenNoCompression_ThenNeverCheaperThanCostA(t *testin x := 100000 filteredTokens := x // no compression: same as context for turns := 1; turns <= 200; turns++ { - costA := CumulativeCostA(turns, x, testParams, PricingOpus) - costB := CumulativeCostB(turns, x, filteredTokens, testParams, PricingOpus) + costA := CumulativeCostA(turns, x, testParams, testPricing) + costB := CumulativeCostB(turns, x, filteredTokens, testParams, testPricing) if costB < costA { t.Errorf("turns=%d: CostB (%.10f) < CostA (%.10f) with no compression; setup cost should never be recovered", turns, costB, costA) return @@ -426,7 +432,7 @@ func Test_ComputeCostMetrics_GivenInjectPages_ThenColdAndWarmMetricsMatchFixture InjectPages: tt.injectPages, } - ComputeCostMetrics(&r, 40000, PricingOpus) + ComputeCostMetrics(&r, 40000, testPricing) if r.BreakEven != tt.wantBreakEven { t.Errorf("BreakEven = %d, want %d", r.BreakEven, tt.wantBreakEven) @@ -459,7 +465,7 @@ func Test_ComputeCostMetrics_GivenCompression_ThenBreakEvenExists(t *testing.T) AvgResponse: 2000, Prompt: 10000, } - ComputeCostMetrics(&r, 40000, PricingOpus) + ComputeCostMetrics(&r, 40000, testPricing) if r.BreakEven <= 0 { t.Errorf("BreakEven = %d, want > 0 when filteredTokens < contextTokens", r.BreakEven) @@ -481,7 +487,7 @@ func Test_ComputeCostMetrics_GivenCompression_ThenWarmBreakEvenAtLeastBreakEven( AvgResponse: 2000, Prompt: 10000, } - ComputeCostMetrics(&r, 40000, PricingOpus) + ComputeCostMetrics(&r, 40000, testPricing) if r.WarmBreakEven != -1 && r.BreakEven != -1 && r.WarmBreakEven < r.BreakEven { t.Errorf("WarmBreakEven (%d) < BreakEven (%d); warm cache is harder to beat, so WarmBreakEven must be >= BreakEven", r.WarmBreakEven, r.BreakEven) @@ -497,7 +503,7 @@ func Test_ComputeCostMetrics_GivenNoCompression_ThenBreakEvenNeverReached(t *tes AvgResponse: 2000, Prompt: 10000, } - ComputeCostMetrics(&r, 40000, PricingOpus) + ComputeCostMetrics(&r, 40000, testPricing) if r.BreakEven != -1 { t.Errorf("BreakEven = %d, want -1 when filteredTokens == contextTokens (no compression)", r.BreakEven) diff --git a/internal/benchmark/model.go b/internal/benchmark/model.go index 222cf45..8db0241 100644 --- a/internal/benchmark/model.go +++ b/internal/benchmark/model.go @@ -1,72 +1,10 @@ package benchmark -import "fmt" +import "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" -// Pricing holds per-million-token rates for a model tier. -type Pricing struct { - CachedRead float64 // $/M tokens - CacheWrite float64 // $/M tokens - BaseInput float64 // $/M tokens (uncached, after last breakpoint) -} - -// PricingOpus is the shared Opus 4.x pricing (4.6, 4.7, 4.8). -var PricingOpus = Pricing{CachedRead: 0.50, CacheWrite: 6.25, BaseInput: 5.00} -var PricingSonnet46 = Pricing{CachedRead: 0.30, CacheWrite: 3.75, BaseInput: 3.00} - -// PricingSonnet55 is Claude Sonnet 5.5's pricing ($2 input / $10 output per -// MTok). Cache read stays at 10% of base input, like Sonnet 4.6. -var PricingSonnet55 = Pricing{CachedRead: 0.20, CacheWrite: 2.50, BaseInput: 2.00} - -// PricingOpus55 is Claude Opus 5.5's pricing. Its cache read is 5% of base -// input rather than the 10% Opus 4.x/Sonnet use (source: Anthropic pricing -// docs, "a cache hit costs 5% of the standard input price"), so its -// cost-savings numbers are not directly comparable to the Opus 4.x/Sonnet rows. -var PricingOpus55 = Pricing{CachedRead: 0.20, CacheWrite: 5.00, BaseInput: 4.00} - -// PricingFable is Claude Fable 5.1's pricing. Its cache read is 2.5% of base -// input — lower than the 10% Opus 4.x/Sonnet use and the 5% Opus 5.5 uses -// (source: Anthropic pricing docs, "a cache hit costs 2.5% of the standard -// input price"), so its cost-savings numbers are not directly comparable to -// the other rows. -var PricingFable = Pricing{CachedRead: 0.25, CacheWrite: 12.50, BaseInput: 10.00} - -const ( - TokenCountModelOpus46 = "claude-opus-4-6" - TokenCountModelOpus47 = "claude-opus-4-7" - TokenCountModelOpus48 = "claude-opus-4-8" - TokenCountModelOpus55 = "claude-opus-5-5" - TokenCountModelSonnet46 = "claude-sonnet-4-6" - TokenCountModelSonnet55 = "claude-sonnet-5-5" - TokenCountModelFable = "claude-fable-5-1" -) - -// ModelConfig bundles the pricing and token-counting model for a given model alias. -type ModelConfig struct { - Pricing Pricing - TokenCountModel string -} - -// ResolveModel maps a user-facing model alias to its ModelConfig. -func ResolveModel(model string) (ModelConfig, error) { - switch model { - case "sonnet", "sonnet-5-5": - return ModelConfig{Pricing: PricingSonnet55, TokenCountModel: TokenCountModelSonnet55}, nil - case "sonnet-4-6": - return ModelConfig{Pricing: PricingSonnet46, TokenCountModel: TokenCountModelSonnet46}, nil - case "opus", "opus-5-5": - return ModelConfig{Pricing: PricingOpus55, TokenCountModel: TokenCountModelOpus55}, nil - case "opus-4-8": - return ModelConfig{Pricing: PricingOpus, TokenCountModel: TokenCountModelOpus48}, nil - case "opus-4-7": - return ModelConfig{Pricing: PricingOpus, TokenCountModel: TokenCountModelOpus47}, nil - case "opus-4-6": - return ModelConfig{Pricing: PricingOpus, TokenCountModel: TokenCountModelOpus46}, nil - case "fable", "fable-5-1": - return ModelConfig{Pricing: PricingFable, TokenCountModel: TokenCountModelFable}, nil - default: - return ModelConfig{}, fmt.Errorf("unknown model %q: must be opus, opus-4-6, opus-4-7, opus-4-8, opus-5-5, sonnet, sonnet-4-6, sonnet-5-5, fable, or fable-5-1", model) - } -} +// Pricing is the per-million-token rate set; the definitions live in +// internal/models. +type Pricing = models.Pricing // RatioPct returns the ratio of NewContextTokens to ContextTokens as a percentage. func (r Result) RatioPct() float64 { diff --git a/internal/benchmark/model_test.go b/internal/benchmark/model_test.go deleted file mode 100644 index 3fc93b1..0000000 --- a/internal/benchmark/model_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package benchmark - -import ( - "strings" - "testing" -) - -func Test_ResolveModel_GivenKnownAlias_ThenReturnsExpectedConfig(t *testing.T) { - tests := []struct { - name string - alias string - wantPricing Pricing - wantTokenCount string - }{ - { - name: "sonnet", - alias: "sonnet", - wantPricing: PricingSonnet55, - wantTokenCount: TokenCountModelSonnet55, - }, - { - name: "sonnet-4-6", - alias: "sonnet-4-6", - wantPricing: PricingSonnet46, - wantTokenCount: TokenCountModelSonnet46, - }, - { - name: "sonnet-5-5", - alias: "sonnet-5-5", - wantPricing: PricingSonnet55, - wantTokenCount: TokenCountModelSonnet55, - }, - { - name: "opus", - alias: "opus", - wantPricing: PricingOpus55, - wantTokenCount: TokenCountModelOpus55, - }, - { - name: "opus-4-8", - alias: "opus-4-8", - wantPricing: PricingOpus, - wantTokenCount: TokenCountModelOpus48, - }, - { - name: "opus-4-7", - alias: "opus-4-7", - wantPricing: PricingOpus, - wantTokenCount: TokenCountModelOpus47, - }, - { - name: "opus-4-6", - alias: "opus-4-6", - wantPricing: PricingOpus, - wantTokenCount: TokenCountModelOpus46, - }, - { - name: "opus-5-5", - alias: "opus-5-5", - wantPricing: PricingOpus55, - wantTokenCount: TokenCountModelOpus55, - }, - { - name: "fable", - alias: "fable", - wantPricing: PricingFable, - wantTokenCount: TokenCountModelFable, - }, - { - name: "fable-5-1", - alias: "fable-5-1", - wantPricing: PricingFable, - wantTokenCount: TokenCountModelFable, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ResolveModel(tt.alias) - if err != nil { - t.Fatalf("ResolveModel(%q) returned error: %v", tt.alias, err) - } - if got.Pricing != tt.wantPricing { - t.Errorf("Pricing = %+v, want %+v", got.Pricing, tt.wantPricing) - } - if got.TokenCountModel != tt.wantTokenCount { - t.Errorf("TokenCountModel = %q, want %q", got.TokenCountModel, tt.wantTokenCount) - } - }) - } -} - -// Regression guard: PricingFable's cache read must stay at 2.5% of base input -// ($0.25/MTok), not the 10% ratio Opus 4.x/Sonnet use. -func Test_PricingFable_ThenCacheReadIsTwoPointFivePercentOfBaseInput(t *testing.T) { - want := PricingFable.BaseInput * 0.025 - if PricingFable.CachedRead != want { - t.Errorf("PricingFable.CachedRead = %v, want %v (2.5%% of BaseInput %v)", - PricingFable.CachedRead, want, PricingFable.BaseInput) - } -} - -// Regression guard: PricingOpus55's cache read must stay at 5% of base input -// ($0.20/MTok), not the 10% ratio Opus 4.x/Sonnet use. -func Test_PricingOpus55_ThenCacheReadIsFivePercentOfBaseInput(t *testing.T) { - want := PricingOpus55.BaseInput * 0.05 - if PricingOpus55.CachedRead != want { - t.Errorf("PricingOpus55.CachedRead = %v, want %v (5%% of BaseInput %v)", - PricingOpus55.CachedRead, want, PricingOpus55.BaseInput) - } -} - -func Test_ResolveModel_GivenUnknownAlias_ThenErrorListsFableAliases(t *testing.T) { - _, err := ResolveModel("nonsense") - if err == nil { - t.Fatal("ResolveModel(\"nonsense\") returned nil error, want unknown model error") - } - if !strings.Contains(err.Error(), "fable") { - t.Errorf("error = %v, want it to list the fable aliases", err) - } -} - -func Test_ResolveModel_GivenUnknownAlias_ThenErrorListsOpus55Alias(t *testing.T) { - _, err := ResolveModel("nonsense") - if err == nil { - t.Fatal("ResolveModel(\"nonsense\") returned nil error, want unknown model error") - } - if !strings.Contains(err.Error(), "opus-5-5") { - t.Errorf("error = %v, want it to list the opus-5-5 alias", err) - } -} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..c4d0904 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,112 @@ +// Package models is the single source of truth for the Claude models this +// tool knows about: their API IDs, per-token pricing, user-facing aliases and +// the default model used for token counting. +// +// To add a model: declare it below, append it to registry, and update +// Latest/DefaultCountTokens if it becomes the newest in its family. Help text +// and error messages derive from registry, so nothing else needs editing +// (docs/benchmark.md is the one place that stays hand-written). +package models + +import ( + "fmt" + "strings" +) + +// Pricing holds per-million-token rates for a model tier. +type Pricing struct { + CachedRead float64 // $/M tokens + CacheWrite float64 // $/M tokens + BaseInput float64 // $/M tokens (uncached, after last breakpoint) +} + +// Model describes one Claude model. +type Model struct { + Name string // canonical alias, e.g. "opus-5-5" + Family string // family alias when this is the newest in its family, e.g. "opus"; "" otherwise + ID string // Anthropic API model ID + Pricing Pricing +} + +// Alias returns the shortest user-facing alias: the family alias when the +// model is the newest in its family, otherwise its canonical name. +func (m Model) Alias() string { + if m.Family != "" { + return m.Family + } + return m.Name +} + +// Opus 4.x share one price list (4.6, 4.7, 4.8). +var pricingOpus4x = Pricing{CachedRead: 0.50, CacheWrite: 6.25, BaseInput: 5.00} + +var ( + // Sonnet 5.5 is $2 input / $10 output per MTok. Cache read is 10% of base + // input, like Sonnet 4.6. + Sonnet55 = Model{Name: "sonnet-5-5", Family: "sonnet", ID: "claude-sonnet-5-5", + Pricing: Pricing{CachedRead: 0.20, CacheWrite: 2.50, BaseInput: 2.00}} + Sonnet46 = Model{Name: "sonnet-4-6", ID: "claude-sonnet-4-6", + Pricing: Pricing{CachedRead: 0.30, CacheWrite: 3.75, BaseInput: 3.00}} + + // Opus 5.5's cache read is 5% of base input rather than the 10% Opus 4.x and + // Sonnet use (Anthropic pricing docs: "a cache hit costs 5% of the standard + // input price"), so its cost-savings numbers are not directly comparable to + // those rows. + Opus55 = Model{Name: "opus-5-5", Family: "opus", ID: "claude-opus-5-5", + Pricing: Pricing{CachedRead: 0.20, CacheWrite: 5.00, BaseInput: 4.00}} + Opus48 = Model{Name: "opus-4-8", ID: "claude-opus-4-8", Pricing: pricingOpus4x} + Opus47 = Model{Name: "opus-4-7", ID: "claude-opus-4-7", Pricing: pricingOpus4x} + Opus46 = Model{Name: "opus-4-6", ID: "claude-opus-4-6", Pricing: pricingOpus4x} + + // Fable 5.1's cache read is 2.5% of base input (Anthropic pricing docs: "a + // cache hit costs 2.5% of the standard input price"), lower than the ratios + // above, so its cost-savings numbers are not directly comparable either. + Fable51 = Model{Name: "fable-5-1", Family: "fable", ID: "claude-fable-5-1", + Pricing: Pricing{CachedRead: 0.25, CacheWrite: 12.50, BaseInput: 10.00}} +) + +// DefaultBenchmark is the model `cc-session benchmark` uses without --model. +var DefaultBenchmark = Opus55 + +// DefaultCountTokens is the model used for count_tokens when none is given. +var DefaultCountTokens = Sonnet55 + +// registry lists every known model, in the order aliases are shown to users. +var registry = []Model{Opus55, Opus48, Opus47, Opus46, Sonnet55, Sonnet46, Fable51} + +// Aliases returns every accepted alias: each family alias followed by its +// canonical name, in registry order. +func Aliases() []string { + var out []string + for _, m := range registry { + if m.Family != "" { + out = append(out, m.Family) + } + out = append(out, m.Name) + } + return out +} + +// Resolve maps a user-facing alias (family alias or canonical name) to its Model. +func Resolve(alias string) (Model, error) { + for _, m := range registry { + if alias == m.Name || (m.Family != "" && alias == m.Family) { + return m, nil + } + } + return Model{}, fmt.Errorf("unknown model %q: must be one of %s", alias, strings.Join(Aliases(), ", ")) +} + +// Usage renders the accepted aliases for flag help, e.g. +// "opus (opus-5-5), opus-4-8, ..., sonnet (sonnet-5-5), ...". +func Usage() string { + parts := make([]string, 0, len(registry)) + for _, m := range registry { + if m.Family != "" { + parts = append(parts, fmt.Sprintf("%s (%s)", m.Family, m.Name)) + } else { + parts = append(parts, m.Name) + } + } + return strings.Join(parts, ", ") +} diff --git a/internal/models/models_test.go b/internal/models/models_test.go new file mode 100644 index 0000000..2492249 --- /dev/null +++ b/internal/models/models_test.go @@ -0,0 +1,98 @@ +package models + +import ( + "math" + "strings" + "testing" +) + +func Test_Resolve_GivenKnownAlias_ThenReturnsExpectedModel(t *testing.T) { + tests := []struct { + alias string + want Model + }{ + {"opus", Opus55}, + {"opus-5-5", Opus55}, + {"opus-4-8", Opus48}, + {"opus-4-7", Opus47}, + {"opus-4-6", Opus46}, + {"sonnet", Sonnet55}, + {"sonnet-5-5", Sonnet55}, + {"sonnet-4-6", Sonnet46}, + {"fable", Fable51}, + {"fable-5-1", Fable51}, + } + for _, tt := range tests { + t.Run(tt.alias, func(t *testing.T) { + got, err := Resolve(tt.alias) + if err != nil { + t.Fatalf("Resolve(%q) returned error: %v", tt.alias, err) + } + if got != tt.want { + t.Errorf("Resolve(%q) = %+v, want %+v", tt.alias, got, tt.want) + } + }) + } +} + +func Test_Resolve_GivenUnknownAlias_ThenErrorListsEveryAlias(t *testing.T) { + _, err := Resolve("nonsense") + if err == nil { + t.Fatal("Resolve(\"nonsense\") returned nil error") + } + for _, alias := range Aliases() { + if !strings.Contains(err.Error(), alias) { + t.Errorf("error %q missing alias %q", err, alias) + } + } +} + +func Test_Registry_ThenEveryModelIsResolvableAndFamiliesAreUnique(t *testing.T) { + families := map[string]string{} + names := map[string]bool{} + ids := map[string]bool{} + for _, m := range registry { + if names[m.Name] || ids[m.ID] { + t.Errorf("duplicate name or ID for %+v", m) + } + names[m.Name], ids[m.ID] = true, true + if m.Family != "" { + if prev, dup := families[m.Family]; dup { + t.Errorf("family %q claimed by both %s and %s", m.Family, prev, m.Name) + } + families[m.Family] = m.Name + } + if got, err := Resolve(m.Name); err != nil || got != m { + t.Errorf("Resolve(%q) = %+v, %v; want %+v", m.Name, got, err, m) + } + } +} + +func Test_Defaults_ThenAreRegisteredModels(t *testing.T) { + for name, d := range map[string]Model{"DefaultBenchmark": DefaultBenchmark, "DefaultCountTokens": DefaultCountTokens} { + if got, err := Resolve(d.Name); err != nil || got != d { + t.Errorf("%s = %+v is not in the registry", name, d) + } + } +} + +// Cache-read ratios differ per model and are not comparable across rows; pin +// them so a price edit that breaks the ratio is caught. +func Test_Pricing_ThenCacheReadRatiosMatchPricingDocs(t *testing.T) { + tests := []struct { + m Model + ratio float64 + }{ + {Sonnet55, 0.10}, {Sonnet46, 0.10}, {Opus48, 0.10}, + {Opus55, 0.05}, {Fable51, 0.025}, + } + for _, tt := range tests { + if want := tt.m.Pricing.BaseInput * tt.ratio; math.Abs(tt.m.Pricing.CachedRead-want) > 1e-9 { + t.Errorf("%s CachedRead = %v, want %v (%v of BaseInput %v)", + tt.m.Name, tt.m.Pricing.CachedRead, want, tt.ratio, tt.m.Pricing.BaseInput) + } + if want := tt.m.Pricing.BaseInput * 1.25; math.Abs(tt.m.Pricing.CacheWrite-want) > 1e-9 { + t.Errorf("%s CacheWrite = %v, want %v (1.25x BaseInput)", tt.m.Name, tt.m.Pricing.CacheWrite, want) + } + } +} diff --git a/internal/tokens/tokens.go b/internal/tokens/tokens.go index 065d1d7..1c95947 100644 --- a/internal/tokens/tokens.go +++ b/internal/tokens/tokens.go @@ -14,6 +14,7 @@ import ( "time" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/config" + "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" ) const ( @@ -29,7 +30,8 @@ const ( baseRetryDelay = 500 * time.Millisecond ) -const DefaultCountTokensModel = "claude-sonnet-5-5" +// DefaultCountTokensModel is the model used when none is specified. +var DefaultCountTokensModel = models.DefaultCountTokens.ID // CountTokensAPI calls the Anthropic count_tokens endpoint. // Resolves the API key from: env ANTHROPIC_API_KEY → config file path in From 92647b8a1e72c2aac5080f90e7108a14c5734f1a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 19:22:43 +0000 Subject: [PATCH 2/3] refactor: drop Pricing alias and DefaultCountTokensModel indirection Reference models.Pricing and models.DefaultCountTokens.ID directly instead of re-exporting them, and trim redundant comments. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HX8KccseqQb2rUserM2BDT --- internal/benchmark/cost.go | 16 +++++++++------- internal/benchmark/cost_test.go | 3 +-- internal/benchmark/model.go | 6 ------ internal/benchmark/render.go | 5 +++-- internal/models/models.go | 11 +++-------- internal/tokens/tokens.go | 7 ++----- internal/tokens/tokens_test.go | 16 +++++++++------- 7 files changed, 27 insertions(+), 37 deletions(-) diff --git a/internal/benchmark/cost.go b/internal/benchmark/cost.go index 48fea7d..5f0ef5f 100644 --- a/internal/benchmark/cost.go +++ b/internal/benchmark/cost.go @@ -1,5 +1,7 @@ package benchmark +import "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" + // Cost model. // // Pricing: https://platform.claude.com/docs/en/build-with-claude/prompt-caching @@ -95,7 +97,7 @@ func NewCostParams(r Result, overheadTokens int) CostParams { // // Prefix grows across turns by growth + (K-1)*toolIO per turn, because tool I/O // from previous turns stays in the conversation history. -func CumulativeCostA(turns int, x int, sp CostParams, p Pricing) float64 { +func CumulativeCostA(turns int, x int, sp CostParams, p models.Pricing) float64 { total := 0.0 extraCalls := extraCallsPerTurn(sp.K) s := sp.ToolIOPerCall @@ -125,7 +127,7 @@ func CumulativeCostA(turns int, x int, sp CostParams, p Pricing) float64 { // // Call 1: cache read (prefix from previous turn, or X when N=1) + cache write (R + P) // Calls 2..K: cache read (growing prefix) + cache write (tool I/O) -func CumulativeCostAWarm(turns int, x int, sp CostParams, p Pricing) float64 { +func CumulativeCostAWarm(turns int, x int, sp CostParams, p models.Pricing) float64 { total := 0.0 extraCalls := extraCallsPerTurn(sp.K) s := sp.ToolIOPerCall @@ -152,7 +154,7 @@ func CumulativeCostAWarm(turns int, x int, sp CostParams, p Pricing) float64 { // Calls 2..K: cache read (growing) + cache write (tool I/O) // // Turn N (N>=2): same structure as A but with smaller base, cross-turn write = growth. -func CumulativeCostB(turns int, x int, filteredTokens int, sp CostParams, p Pricing) float64 { +func CumulativeCostB(turns int, x int, filteredTokens int, sp CostParams, p models.Pricing) float64 { return CumulativeCostBWithInjectPages(turns, filteredTokens, 1, sp, p) } @@ -166,7 +168,7 @@ func CumulativeCostB(turns int, x int, filteredTokens int, sp CostParams, p Pric // each page: read(overhead + previous page tokens) + write(page tokens) // // The active conversation after setup is still overhead + filteredTokens. -func CumulativeCostBWithInjectPages(turns int, filteredTokens int, injectPages int, sp CostParams, p Pricing) float64 { +func CumulativeCostBWithInjectPages(turns int, filteredTokens int, injectPages int, sp CostParams, p models.Pricing) float64 { base := sp.Overhead + filteredTokens total := newSessionSetupCost(filteredTokens, injectPages, sp, p) extraCalls := extraCallsPerTurn(sp.K) @@ -189,7 +191,7 @@ func CumulativeCostBWithInjectPages(turns int, filteredTokens int, injectPages i return total } -func newSessionSetupCost(filteredTokens int, injectPages int, sp CostParams, p Pricing) float64 { +func newSessionSetupCost(filteredTokens int, injectPages int, sp CostParams, p models.Pricing) float64 { base := sp.Overhead + filteredTokens if injectPages <= 1 { return float64(base) * p.CacheWrite / 1e6 @@ -212,7 +214,7 @@ func extraCallsPerTurn(k float64) float64 { return k - 1 } -func intraTurnToolCost(firstPrefix float64, extraCalls float64, toolIOPerCall int, p Pricing) float64 { +func intraTurnToolCost(firstPrefix float64, extraCalls float64, toolIOPerCall int, p models.Pricing) float64 { if extraCalls <= 0 { return 0 } @@ -231,7 +233,7 @@ func intraTurnToolCost(firstPrefix float64, extraCalls float64, toolIOPerCall in } // ComputeCostMetrics populates the cost-related fields of r. -func ComputeCostMetrics(r *Result, overheadTokens int, p Pricing) { +func ComputeCostMetrics(r *Result, overheadTokens int, p models.Pricing) { sp := NewCostParams(*r, overheadTokens) r.BreakEven = -1 for n := 1; n <= 200; n++ { diff --git a/internal/benchmark/cost_test.go b/internal/benchmark/cost_test.go index 9f72b8b..1f27691 100644 --- a/internal/benchmark/cost_test.go +++ b/internal/benchmark/cost_test.go @@ -9,8 +9,7 @@ import ( const epsilon = 1e-9 -// testPricing is the Opus 4.x price list; expected values below assume -// $5 base input, $6.25 cache write, $0.50 cache read. +// Expected values below assume the Opus 4.x rates ($5 / $6.25 / $0.50). var testPricing = models.Opus48.Pricing func approxEqual(got, want float64) bool { diff --git a/internal/benchmark/model.go b/internal/benchmark/model.go index 8db0241..9d9c773 100644 --- a/internal/benchmark/model.go +++ b/internal/benchmark/model.go @@ -1,11 +1,5 @@ package benchmark -import "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" - -// Pricing is the per-million-token rate set; the definitions live in -// internal/models. -type Pricing = models.Pricing - // RatioPct returns the ratio of NewContextTokens to ContextTokens as a percentage. func (r Result) RatioPct() float64 { if r.ContextTokens == 0 { diff --git a/internal/benchmark/render.go b/internal/benchmark/render.go index d797351..483f84a 100644 --- a/internal/benchmark/render.go +++ b/internal/benchmark/render.go @@ -7,6 +7,7 @@ import ( "sort" "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/analyzer" + "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" ) // PrintCompressionSection writes the compression comparison table to out. @@ -44,7 +45,7 @@ func PrintCompressionSection(out io.Writer, results []Result) { } // PrintCostSummary writes the cold-cache cost comparison table to out. -func PrintCostSummary(out io.Writer, results []Result, p Pricing, modelName string) { +func PrintCostSummary(out io.Writer, results []Result, p models.Pricing, modelName string) { if len(results) == 0 { return } @@ -55,7 +56,7 @@ func PrintCostSummary(out io.Writer, results []Result, p Pricing, modelName stri } // PrintWarmCostSummary writes the warm-cache cost comparison table to out. -func PrintWarmCostSummary(out io.Writer, results []Result, p Pricing, modelName string) { +func PrintWarmCostSummary(out io.Writer, results []Result, p models.Pricing, modelName string) { if len(results) == 0 { return } diff --git a/internal/models/models.go b/internal/models/models.go index c4d0904..bd15499 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -2,10 +2,8 @@ // tool knows about: their API IDs, per-token pricing, user-facing aliases and // the default model used for token counting. // -// To add a model: declare it below, append it to registry, and update -// Latest/DefaultCountTokens if it becomes the newest in its family. Help text -// and error messages derive from registry, so nothing else needs editing -// (docs/benchmark.md is the one place that stays hand-written). +// Help text and error messages derive from registry; docs/benchmark.md is the +// only place that must be updated by hand. package models import ( @@ -28,8 +26,7 @@ type Model struct { Pricing Pricing } -// Alias returns the shortest user-facing alias: the family alias when the -// model is the newest in its family, otherwise its canonical name. +// Alias returns the family alias if set, otherwise the canonical name. func (m Model) Alias() string { if m.Family != "" { return m.Family @@ -41,8 +38,6 @@ func (m Model) Alias() string { var pricingOpus4x = Pricing{CachedRead: 0.50, CacheWrite: 6.25, BaseInput: 5.00} var ( - // Sonnet 5.5 is $2 input / $10 output per MTok. Cache read is 10% of base - // input, like Sonnet 4.6. Sonnet55 = Model{Name: "sonnet-5-5", Family: "sonnet", ID: "claude-sonnet-5-5", Pricing: Pricing{CachedRead: 0.20, CacheWrite: 2.50, BaseInput: 2.00}} Sonnet46 = Model{Name: "sonnet-4-6", ID: "claude-sonnet-4-6", diff --git a/internal/tokens/tokens.go b/internal/tokens/tokens.go index 1c95947..742dc3c 100644 --- a/internal/tokens/tokens.go +++ b/internal/tokens/tokens.go @@ -30,14 +30,11 @@ const ( baseRetryDelay = 500 * time.Millisecond ) -// DefaultCountTokensModel is the model used when none is specified. -var DefaultCountTokensModel = models.DefaultCountTokens.ID - // CountTokensAPI calls the Anthropic count_tokens endpoint. // Resolves the API key from: env ANTHROPIC_API_KEY → config file path in // ~/.claude/skills//config.json → error. func CountTokensAPI(text string) (int, error) { - counter, err := NewCounter(DefaultCountTokensModel) + counter, err := NewCounter(models.DefaultCountTokens.ID) if err != nil { return 0, err } @@ -59,7 +56,7 @@ func NewCounter(model string) (*Counter, error) { return nil, fmt.Errorf("ANTHROPIC_API_KEY not set (set env var or configure anthropic_api_key_file in ~/.claude/skills/cc-session/config.json)") } if model == "" { - model = DefaultCountTokensModel + model = models.DefaultCountTokens.ID } return &Counter{ apiKey: apiKey, diff --git a/internal/tokens/tokens_test.go b/internal/tokens/tokens_test.go index 20f79f5..b2f6a0e 100644 --- a/internal/tokens/tokens_test.go +++ b/internal/tokens/tokens_test.go @@ -10,6 +10,8 @@ import ( "sync/atomic" "testing" "time" + + "github.com/Mapleeeeeeeeeee/cc-session-reader/internal/models" ) func TestCountTokensAPI_WhenAPIKeyIsMissing_ThenReturnsError(t *testing.T) { @@ -48,8 +50,8 @@ func TestCountTokens_SendsAnthropicRequestAndParsesResponse(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { t.Fatalf("decode request: %v", err) } - if payload.Model != DefaultCountTokensModel { - t.Fatalf("model = %q, want %q", payload.Model, DefaultCountTokensModel) + if payload.Model != models.DefaultCountTokens.ID { + t.Fatalf("model = %q, want %q", payload.Model, models.DefaultCountTokens.ID) } if len(payload.Messages) != 1 || payload.Messages[0].Role != "user" || payload.Messages[0].Content != "hello" { t.Fatalf("messages = %#v, want one user hello message", payload.Messages) @@ -60,7 +62,7 @@ func TestCountTokens_SendsAnthropicRequestAndParsesResponse(t *testing.T) { })) defer server.Close() - got, err := countTokens("hello", "test-key", server.URL, DefaultCountTokensModel, server.Client()) + got, err := countTokens("hello", "test-key", server.URL, models.DefaultCountTokens.ID, server.Client()) if err != nil { t.Fatalf("countTokens returned error: %v", err) } @@ -105,7 +107,7 @@ func TestCountTokens_WhenAPIReturnsError_ThenIncludesStatus(t *testing.T) { })) defer server.Close() - _, err := countTokens("hello", "test-key", server.URL, DefaultCountTokensModel, server.Client()) + _, err := countTokens("hello", "test-key", server.URL, models.DefaultCountTokens.ID, server.Client()) if err == nil { t.Fatal("countTokens returned nil error, want API status error") } @@ -130,7 +132,7 @@ func TestCountTokens_WhenTransientErrorThenSuccess_ThenRetriesAndSucceeds(t *tes })) defer server.Close() - got, err := countTokens("hello", "test-key", server.URL, DefaultCountTokensModel, server.Client()) + got, err := countTokens("hello", "test-key", server.URL, models.DefaultCountTokens.ID, server.Client()) if err != nil { t.Fatalf("countTokens returned error after retry: %v", err) } @@ -153,7 +155,7 @@ func TestCountTokens_WhenNonTransientError_ThenDoesNotRetry(t *testing.T) { })) defer server.Close() - _, err := countTokens("hello", "test-key", server.URL, DefaultCountTokensModel, server.Client()) + _, err := countTokens("hello", "test-key", server.URL, models.DefaultCountTokens.ID, server.Client()) if err == nil { t.Fatal("countTokens returned nil error, want immediate 400 error") } @@ -186,7 +188,7 @@ func TestCountTokens_When429WithRetryAfter_ThenRespectsHintAndSucceeds(t *testin })) defer server.Close() - got, err := countTokens("hello", "test-key", server.URL, DefaultCountTokensModel, server.Client()) + got, err := countTokens("hello", "test-key", server.URL, models.DefaultCountTokens.ID, server.Client()) if err != nil { t.Fatalf("countTokens returned error after 429 retry: %v", err) } From 5fa06131fef7402302cb0fc85a3c86de68971466 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 19:23:51 +0000 Subject: [PATCH 3/3] test(models): drop redundant round-trip resolve check Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HX8KccseqQb2rUserM2BDT --- internal/models/models_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/models/models_test.go b/internal/models/models_test.go index 2492249..e853e82 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -47,7 +47,7 @@ func Test_Resolve_GivenUnknownAlias_ThenErrorListsEveryAlias(t *testing.T) { } } -func Test_Registry_ThenEveryModelIsResolvableAndFamiliesAreUnique(t *testing.T) { +func Test_Registry_ThenNamesIDsAndFamiliesAreUnique(t *testing.T) { families := map[string]string{} names := map[string]bool{} ids := map[string]bool{} @@ -62,9 +62,6 @@ func Test_Registry_ThenEveryModelIsResolvableAndFamiliesAreUnique(t *testing.T) } families[m.Family] = m.Name } - if got, err := Resolve(m.Name); err != nil || got != m { - t.Errorf("Resolve(%q) = %+v, %v; want %+v", m.Name, got, err, m) - } } }