Skip to content

Refactor/open closed principle#98

Closed
SantiagoDePolonia wants to merge 3 commits intomainfrom
refactor/open-closed-principle
Closed

Refactor/open closed principle#98
SantiagoDePolonia wants to merge 3 commits intomainfrom
refactor/open-closed-principle

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Feb 25, 2026

Summary by CodeRabbit

  • New Features

    • Extended provider cost mappings for cached and reasoning tokens across multiple providers.
    • Added support for informational token fields that don't require separate pricing.
    • Improved configuration documentation clarifying the order of configuration sources.
  • Tests

    • Added cost registry validation tests.
  • Refactor

    • Reorganized startup sequence to register provider cost information earlier in initialization.

SantiagoDePolonia and others added 3 commits February 25, 2026 13:33
Move provider-specific cost mapping data out of usage/cost.go and into
each provider's Registration var, wired through the factory at startup.
This ensures adding a new provider no longer requires editing the usage
package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add CostSideUnknown/CostUnitUnknown zero-value sentinels and field
  doc comments to TokenCostMapping (core/cost_mapping.go)
- Sort informationalFields in CostRegistry for deterministic output
- Add default branches to Unit/Side switches to surface unknown enums
- Use atomic.Pointer for defaultCostRegistry to prevent data races
- Replace hardcoded test data with provider Registration vars
- Add cache_read_input_tokens and cache_creation_input_tokens to
  Anthropic streaming usage (both ChatCompletion and Responses)
- Fix Shutdown docstring and startup sequence in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract duplicated cache field injection in Anthropic streaming into
  appendCacheFields helper, called from both ChatCompletion and
  Responses stream converters
- Use ProviderFactory.CostRegistry() in usage TestMain to aggregate
  informational fields the same way production does, instead of
  hardcoding openai.Registration.InformationalFields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This PR introduces a dynamic provider cost mapping framework by adding a new TokenCostMapping data model with enums for pricing units and sides, extending provider registrations with cost and informational field declarations, refactoring the provider factory to aggregate these mappings via a new CostRegistry() method, and establishing a runtime registration pattern that replaces hard-coded cost lookups with a queryable registry populated during app startup.

Changes

Cohort / File(s) Summary
Core Cost Model
internal/core/cost_mapping.go
Introduces CostSide and CostUnit enums and the TokenCostMapping struct to define per-provider token cost mappings with pricing field accessors and unit/side metadata.
Provider Factory Refactoring
internal/providers/factory.go, internal/providers/factory_test.go
Extends Registration struct with CostMappings and InformationalFields fields; refactors ProviderFactory from a builders map to a registrations map; adds CostRegistry() method to aggregate per-provider cost mappings and informational fields.
Provider Cost Declarations
internal/providers/anthropic/anthropic.go, internal/providers/gemini/gemini.go, internal/providers/openai/openai.go, internal/providers/xai/xai.go
Each provider's Registration now declares provider-specific CostMappings (e.g., cached tokens, reasoning tokens) and optional InformationalFields for non-priced token breakdowns.
Usage Cost Registry
internal/usage/cost.go, internal/usage/stream_wrapper.go
Replaces hard-coded provider mappings with a runtime costRegistry populated via new RegisterCostMappings() function; refactors cost calculation to load mappings from the registry and support new enum-based unit/side constants. Adds test bootstrap in internal/usage/setup_test.go to initialize registry during test execution.
App Initialization
internal/app/app.go
Integrates cost registry registration into app startup by calling cfg.Factory.CostRegistry() and invoking usage.RegisterCostMappings() before audit logging initialization.
Documentation
CLAUDE.md
Updates provider registration template to show new CostMappings and InformationalFields fields; clarifies config cascade precedence and cost-mappings registration step in startup sequence.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Init
    participant Factory as ProviderFactory
    participant Usage as Usage Module
    participant Calc as Cost Calculator
    
    App->>Factory: CostRegistry()
    Factory->>Factory: Aggregate CostMappings<br/>from all registrations
    Factory-->>App: mappings, informationalFields
    
    App->>Usage: RegisterCostMappings(mappings,<br/>informationalFields)
    Usage->>Usage: Store in atomic<br/>costRegistry
    
    rect rgba(100, 150, 255, 0.5)
    Note over Calc: Later at runtime
    Calc->>Usage: loadCostRegistry()
    Usage-->>Calc: costRegistry{providerMappings,<br/>informationalFields}
    Calc->>Calc: Look up RawDataKey mappings<br/>and pricing fields
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 With whiskers twitching at each cost,
Our rabbits track what might be lost,
Per-token prices, mapped with care—
From OpenAI to Claude's lair!
The registry hops, expenses flow,
Now every provider's prices glow! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The pull request title 'Refactor/open closed principle' is vague and does not clearly summarize the main change in a concrete way; it uses a generic architectural principle name without specifying what was refactored or how. Consider a more descriptive title such as 'Move provider-specific cost mappings to Registration struct' or 'Refactor cost mappings to follow Open/Closed Principle' to better communicate the specific nature of the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/open-closed-principle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/usage/stream_wrapper.go (1)

236-240: 🧹 Nitpick | 🔵 Trivial

loadCostRegistry() called on every SSE data line — consider caching the registry reference at wrapper creation.

loadCostRegistry() is invoked inside extractUsageFromJSON, which is called for every data: line in every SSE event. Although an atomic.Pointer load is cheap, the map lookup loop over extendedFieldSet runs repeatedly with the same registry for the lifetime of the wrapper. Caching the extendedFieldSet map reference (or the whole *costRegistry) in StreamUsageWrapper at construction time would be cleaner and avoids repeated atomic loads on the hot path.

♻️ Proposed refactor
 type StreamUsageWrapper struct {
     io.ReadCloser
     logger           LoggerInterface
     pricingResolver  PricingResolver
     eventBuffer      bytes.Buffer
     cachedEntry      *UsageEntry
     model            string
     provider         string
     requestID        string
     endpoint         string
+    extendedFields   map[string]struct{}
     closed           bool
 }

 func NewStreamUsageWrapper(...) *StreamUsageWrapper {
     return &StreamUsageWrapper{
         ...
+        extendedFields: loadCostRegistry().extendedFieldSet,
     }
 }

Then replace Line 236 with:

-	for field := range loadCostRegistry().extendedFieldSet {
+	for field := range w.extendedFields {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/stream_wrapper.go` around lines 236 - 240, Cache the registry
reference when the wrapper is created instead of calling loadCostRegistry() on
every SSE line: add a field to StreamUsageWrapper (e.g., cachedRegistry or
cachedExtendedFieldSet of type map[string]struct{} or *costRegistry) and
initialize it in the constructor, update extractUsageFromJSON to use that
cachedExtendedFieldSet (or cachedRegistry.extendedFieldSet) instead of calling
loadCostRegistry(), and remove the loadCostRegistry() call inside the loop that
iterates over extendedFieldSet so the map lookup uses the pre-stored reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/cost_mapping.go`:
- Around line 7-18: The CostSideUnknown and CostUnitUnknown zero-value sentinels
can slip into TokenCostMapping when providers omit Side or Unit, so add
validation to reject mappings with those zero-values: in the factory.Add (and/or
RegisterCostMappings) code path where TokenCostMapping entries are registered
(the same place that already panics on empty Type or nil New), check each
mapping's Side against CostSideUnknown and Unit against CostUnitUnknown and
panic or return an error if found; ensure the validation references
TokenCostMapping.Side and TokenCostMapping.Unit to make failures explicit and
prevent silent incorrect mappings.

In `@internal/usage/cost.go`:
- Around line 112-113: The code calls m.PricingField(pricing) without ensuring
the PricingField callback exists, which can panic if a TokenCostMapping is
zero-valued; update the logic in the block that currently invokes PricingField
(using variables m and pricing) to first check if m.PricingField != nil, and if
nil emit a caveat/log (e.g., note missing PricingField for that
TokenCostMapping) and skip calling it, otherwise call m.PricingField(pricing) as
before.
- Around line 47-65: RegisterCostMappings currently stores the caller-owned
mappings map directly into the costRegistry, allowing callers to mutate
slices/entries after registration and causing concurrent readers (e.g.,
CalculateGranularCost) to observe mutations; fix by defensively cloning the
entire mappings structure before publishing: allocate a new
map[string][]core.TokenCostMapping, iterate each key in mappings, create a new
slice for each value and copy its TokenCostMapping elements (so slices and
elements are not shared), then assign that new map to
costRegistry.providerMappings (and keep existing cloning of informational into
informationalFields) before calling costRegistryPtr.Store(reg); reference
RegisterCostMappings, costRegistry.providerMappings, core.TokenCostMapping,
RawDataKey, costRegistryPtr, and CalculateGranularCost when locating where to
implement the clone.

In `@internal/usage/setup_test.go`:
- Around line 14-27: Add a short maintenance comment above TestMain explaining
that the provider list (calls to providers.NewProviderFactory() and
factory.Add(...)) must be updated whenever a new provider is introduced so
CostRegistry() and RegisterCostMappings() remain complete; reference TestMain,
providers.NewProviderFactory, factory.Add and RegisterCostMappings in the
comment so future contributors know to add the new provider registrations to
this test setup.

---

Outside diff comments:
In `@internal/usage/stream_wrapper.go`:
- Around line 236-240: Cache the registry reference when the wrapper is created
instead of calling loadCostRegistry() on every SSE line: add a field to
StreamUsageWrapper (e.g., cachedRegistry or cachedExtendedFieldSet of type
map[string]struct{} or *costRegistry) and initialize it in the constructor,
update extractUsageFromJSON to use that cachedExtendedFieldSet (or
cachedRegistry.extendedFieldSet) instead of calling loadCostRegistry(), and
remove the loadCostRegistry() call inside the loop that iterates over
extendedFieldSet so the map lookup uses the pre-stored reference.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28d82ea and 8f305eb.

📒 Files selected for processing (12)
  • CLAUDE.md
  • internal/app/app.go
  • internal/core/cost_mapping.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/factory.go
  • internal/providers/factory_test.go
  • internal/providers/gemini/gemini.go
  • internal/providers/openai/openai.go
  • internal/providers/xai/xai.go
  • internal/usage/cost.go
  • internal/usage/setup_test.go
  • internal/usage/stream_wrapper.go

Comment on lines +7 to +18
CostSideUnknown CostSide = iota // zero value; must not be used in mappings
CostSideInput
CostSideOutput
)

// CostUnit indicates how the pricing field is applied.
type CostUnit int

const (
CostUnitUnknown CostUnit = iota // zero value; must not be used in mappings
CostUnitPerMtok // divide token count by 1M, multiply by rate
CostUnitPerItem // multiply count directly by rate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

CostSideUnknown/CostUnitUnknown zero-value sentinels are unvalidated — silently incorrect mappings possible.

The doc comments say these values "must not be used in mappings," but nothing enforces this. A provider that accidentally omits Side or Unit in a TokenCostMapping literal will compile and run without error, producing a mapping with a zero-value enum that the cost-calculation logic likely cannot handle correctly. Consider adding a validation guard in factory.Add() (which already panics on empty Type/nil New) or in RegisterCostMappings:

🛡️ Suggested validation in factory.Add (illustrative)
// Inside factory.Add, after existing guards:
+for i, m := range reg.CostMappings {
+    if m.RawDataKey == "" {
+        panic(fmt.Sprintf("provider %q: CostMappings[%d] has empty RawDataKey", reg.Type, i))
+    }
+    if m.Side == core.CostSideUnknown {
+        panic(fmt.Sprintf("provider %q: CostMappings[%d] has unknown Side", reg.Type, i))
+    }
+    if m.Unit == core.CostUnitUnknown {
+        panic(fmt.Sprintf("provider %q: CostMappings[%d] has unknown Unit", reg.Type, i))
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/cost_mapping.go` around lines 7 - 18, The CostSideUnknown and
CostUnitUnknown zero-value sentinels can slip into TokenCostMapping when
providers omit Side or Unit, so add validation to reject mappings with those
zero-values: in the factory.Add (and/or RegisterCostMappings) code path where
TokenCostMapping entries are registered (the same place that already panics on
empty Type or nil New), check each mapping's Side against CostSideUnknown and
Unit against CostUnitUnknown and panic or return an error if found; ensure the
validation references TokenCostMapping.Side and TokenCostMapping.Unit to make
failures explicit and prevent silent incorrect mappings.

Comment thread internal/usage/cost.go
Comment on lines +47 to +65
func RegisterCostMappings(mappings map[string][]core.TokenCostMapping, informational []string) {
reg := &costRegistry{
providerMappings: mappings,
informationalFields: make(map[string]struct{}, len(informational)),
extendedFieldSet: make(map[string]struct{}),
}

// extendedFieldSet is derived from providerMappings and contains all RawData keys
// that providers may report. Used by stream_wrapper.go to extract extended fields
// from SSE usage data without maintaining a separate hard-coded list.
var extendedFieldSet = func() map[string]struct{} {
set := make(map[string]struct{})
for _, mappings := range providerMappings {
for _, m := range mappings {
set[m.rawDataKey] = struct{}{}
for _, f := range informational {
reg.informationalFields[f] = struct{}{}
}

for _, ms := range mappings {
for _, m := range ms {
reg.extendedFieldSet[m.RawDataKey] = struct{}{}
}
}
return set
}()

costRegistryPtr.Store(reg)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /repo && find internal/usage -type f -name "*.go" | head -20

Repository: ENTERPILOT/GOModel

Length of output: 117


🏁 Script executed:

find . -path "*/internal/usage/*.go" -type f

Repository: ENTERPILOT/GOModel

Length of output: 758


🏁 Script executed:

cat -n ./internal/usage/cost.go | head -100

Repository: ENTERPILOT/GOModel

Length of output: 3761


🏁 Script executed:

cat -n ./internal/usage/cost.go | sed -n '100,200p'

Repository: ENTERPILOT/GOModel

Length of output: 3354


🏁 Script executed:

grep -rn "RegisterCostMappings" --include="*.go" .

Repository: ENTERPILOT/GOModel

Length of output: 655


🏁 Script executed:

cat -n ./internal/app/app.go | sed -n '70,85p'

Repository: ENTERPILOT/GOModel

Length of output: 751


🏁 Script executed:

grep -rn "CostRegistry" --include="*.go" . | head -20

Repository: ENTERPILOT/GOModel

Length of output: 1140


🏁 Script executed:

cat -n ./internal/providers/factory.go | sed -n '106,140p'

Repository: ENTERPILOT/GOModel

Length of output: 1005


🏁 Script executed:

cat -n ./internal/usage/cost_test.go | head -80

Repository: ENTERPILOT/GOModel

Length of output: 3135


🏁 Script executed:

cat -n ./internal/providers/factory.go | sed -n '1,50p'

Repository: ENTERPILOT/GOModel

Length of output: 1918


🏁 Script executed:

cat -n ./internal/providers/factory.go | sed -n '115,120p'

Repository: ENTERPILOT/GOModel

Length of output: 304


🏁 Script executed:

cat -n ./internal/usage/setup_test.go

Repository: ENTERPILOT/GOModel

Length of output: 978


🏁 Script executed:

rg -n "costRegistryPtr|loadCostRegistry" --include="*.go" -B 2 -A 2

Repository: ENTERPILOT/GOModel

Length of output: 503


🏁 Script executed:

rg -n "costRegistryPtr|loadCostRegistry" -t go -B 2 -A 2

Repository: ENTERPILOT/GOModel

Length of output: 1867


🏁 Script executed:

rg -n "costMappings\|RegisterCostMappings" -t go -B 3 -A 3 ./internal/app/app.go

Repository: ENTERPILOT/GOModel

Length of output: 44


🏁 Script executed:

cat -n ./internal/app/app.go | sed -n '60,90p'

Repository: ENTERPILOT/GOModel

Length of output: 1180


Defensively copy mappings before publishing the registry.

Line 49 stores the caller-owned map by reference. While current usage calls this once at startup, if the input map or slices are modified after registration, concurrent readers in CalculateGranularCost could observe mutations and panic. Clone map and slices before Store().

Suggested fix
 func RegisterCostMappings(mappings map[string][]core.TokenCostMapping, informational []string) {
+	copiedMappings := make(map[string][]core.TokenCostMapping, len(mappings))
+	for providerType, providerMappings := range mappings {
+		mappingsCopy := make([]core.TokenCostMapping, len(providerMappings))
+		copy(mappingsCopy, providerMappings)
+		copiedMappings[providerType] = mappingsCopy
+	}
+
 	reg := &costRegistry{
-		providerMappings:    mappings,
+		providerMappings:    copiedMappings,
 		informationalFields: make(map[string]struct{}, len(informational)),
 		extendedFieldSet:    make(map[string]struct{}),
 	}
@@
-	for _, ms := range mappings {
+	for _, ms := range copiedMappings {
 		for _, m := range ms {
 			reg.extendedFieldSet[m.RawDataKey] = struct{}{}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/cost.go` around lines 47 - 65, RegisterCostMappings currently
stores the caller-owned mappings map directly into the costRegistry, allowing
callers to mutate slices/entries after registration and causing concurrent
readers (e.g., CalculateGranularCost) to observe mutations; fix by defensively
cloning the entire mappings structure before publishing: allocate a new
map[string][]core.TokenCostMapping, iterate each key in mappings, create a new
slice for each value and copy its TokenCostMapping elements (so slices and
elements are not shared), then assign that new map to
costRegistry.providerMappings (and keep existing cloning of informational into
informationalFields) before calling costRegistryPtr.Store(reg); reference
RegisterCostMappings, costRegistry.providerMappings, core.TokenCostMapping,
RawDataKey, costRegistryPtr, and CalculateGranularCost when locating where to
implement the clone.

Comment thread internal/usage/cost.go
Comment on lines +112 to 113
rate := m.PricingField(pricing)
if rate == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the cost.go file
find . -path "*internal/usage/cost.go" -type f

Repository: ENTERPILOT/GOModel

Length of output: 86


🏁 Script executed:

# Read the file to see the context around lines 112-113
cat -n internal/usage/cost.go | head -130 | tail -50

Repository: ENTERPILOT/GOModel

Length of output: 1938


🏁 Script executed:

# Let's also check the type definition of TokenCostMapping
fd "\.go$" -x grep -l "type TokenCostMapping" {}

Repository: ENTERPILOT/GOModel

Length of output: 93


🏁 Script executed:

# Search for PricingField usage patterns in the codebase
rg "PricingField" -A 3 -B 3 internal/usage/

Repository: ENTERPILOT/GOModel

Length of output: 811


🏁 Script executed:

# Let's examine the type definition of TokenCostMapping
cat -n internal/core/cost_mapping.go

Repository: ENTERPILOT/GOModel

Length of output: 1389


🏁 Script executed:

# Let's check the test that mentions NilPricingFieldNoCaveat
rg "NilPricingFieldNoCaveat" -A 30 internal/usage/cost_test.go

Repository: ENTERPILOT/GOModel

Length of output: 1169


🏁 Script executed:

# Let's check what initializes the providerMappings and how TokenCostMapping is constructed
rg "providerMappings\[" -B 5 -A 5 internal/

Repository: ENTERPILOT/GOModel

Length of output: 778


🏁 Script executed:

# Search for all TokenCostMapping struct literals to see how they're initialized
rg "TokenCostMapping\{" -A 10 internal/

Repository: ENTERPILOT/GOModel

Length of output: 6606


🏁 Script executed:

# Check if there are any other usages of m.PricingField that show nil checking patterns
rg "\.PricingField" -B 3 -A 3 internal/

Repository: ENTERPILOT/GOModel

Length of output: 394


Guard nil PricingField callbacks before invocation.

Line 112 calls m.PricingField(pricing) unconditionally. If a TokenCostMapping is zero-valued or malformed, the function pointer will be nil and cause a panic. Add a nil check and emit a caveat instead.

Suggested fix
-			rate := m.PricingField(pricing)
+			if m.PricingField == nil {
+				caveats = append(caveats, fmt.Sprintf("missing pricing field mapper for field %s", m.RawDataKey))
+				continue
+			}
+			rate := m.PricingField(pricing)
 			if rate == nil {
 				continue // Base rate covers this token type; no adjustment needed
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rate := m.PricingField(pricing)
if rate == nil {
if m.PricingField == nil {
caveats = append(caveats, fmt.Sprintf("missing pricing field mapper for field %s", m.RawDataKey))
continue
}
rate := m.PricingField(pricing)
if rate == nil {
continue // Base rate covers this token type; no adjustment needed
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/cost.go` around lines 112 - 113, The code calls
m.PricingField(pricing) without ensuring the PricingField callback exists, which
can panic if a TokenCostMapping is zero-valued; update the logic in the block
that currently invokes PricingField (using variables m and pricing) to first
check if m.PricingField != nil, and if nil emit a caveat/log (e.g., note missing
PricingField for that TokenCostMapping) and skip calling it, otherwise call
m.PricingField(pricing) as before.

Comment on lines +14 to +27
func TestMain(m *testing.M) {
// Build cost mappings and informational fields the same way production does:
// register all providers into a factory and use CostRegistry to aggregate.
factory := providers.NewProviderFactory()
factory.Add(openai.Registration)
factory.Add(anthropic.Registration)
factory.Add(gemini.Registration)
factory.Add(xai.Registration)

costMappings, informationalFields := factory.CostRegistry()
RegisterCostMappings(costMappings, informationalFields)

os.Exit(m.Run())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LGTM — mirrors the production startup sequence faithfully.

The TestMain calls factory.CostRegistry() + RegisterCostMappings before m.Run(), exactly mirroring app.go's startup path. The os.Exit(m.Run()) idiom is correct.

One maintenance note: if a new provider is added to the codebase, this TestMain must be updated to include it, otherwise usage package tests will silently run with an incomplete registry (missing the new provider's extendedFieldSet keys). Consider adding a comment to this effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/setup_test.go` around lines 14 - 27, Add a short maintenance
comment above TestMain explaining that the provider list (calls to
providers.NewProviderFactory() and factory.Add(...)) must be updated whenever a
new provider is introduced so CostRegistry() and RegisterCostMappings() remain
complete; reference TestMain, providers.NewProviderFactory, factory.Add and
RegisterCostMappings in the comment so future contributors know to add the new
provider registrations to this test setup.

@SantiagoDePolonia SantiagoDePolonia deleted the refactor/open-closed-principle branch March 22, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant