Skip to content

Conversation

@GAlexIHU
Copy link
Contributor

@GAlexIHU GAlexIHU commented Oct 29, 2025

Overview

Exposes new field

Summary by CodeRabbit

  • New Features
    • Entitlement/usage responses and balance views now include a total available grant amount, showing the cumulative sum of active grants at query time for clearer financial visibility.
    • Metered entitlement and snapshot outputs surface this value alongside balance, usage, overage, and period details for improved reporting.

@GAlexIHU GAlexIHU requested a review from a team as a code owner October 29, 2025 16:36
@GAlexIHU GAlexIHU added the kind/feature New feature or request label Oct 29, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

📝 Walkthrough

Walkthrough

Threads a new TotalAvailableGrantAmount field through the entitlement value chain (API spec, snapshots, metered connector, balance calculation, engine) and captures RunParams in RunResult; also makes BalanceConnector.GetBalanceSinceSnapshot unexported and switches callers to GetBalanceAt. Tests updated to expect the new field.

Changes

Cohort / File(s) Summary
API Spec & Snapshot Model
api/spec/src/entitlements/subjects.tsp, openmeter/entitlement/snapshot/event.go
Adds public totalAvailableGrantAmount / TotalAvailableGrantAmount field to EntitlementValue (spec + snapshot event model)
API Response Mapping
openmeter/entitlement/driver/parser.go
Maps engine/snapshot TotalAvailableGrantAmount into API EntitlementValue (MapEntitlementValueToAPI)
Credit Engine
openmeter/credit/engine/engine.go, openmeter/credit/engine/run.go
Adds RunParams.TotalAvailableGrantAmount() and RunParams.Clone(); stores RunParams in RunResult and returns cloned params in runs
Balance / Connector Changes
openmeter/credit/balance.go, openmeter/entitlement/metered/balance.go, openmeter/entitlement/metered/connector.go
Makes GetBalanceSinceSnapshot unexported and switches callers to GetBalanceAt; adds TotalAvailableGrantAmount to entitlement balance and metered value; populates it from engine run params
Tests
e2e/e2e_test.go
Updates expected EntitlementValue struct to include TotalAvailableGrantAmount *float64

Sequence Diagram(s)

sequenceDiagram
    rect rgb(240,248,255)
    participant API as API / Handler
    participant Conn as Metered Connector
    participant Balance as Entitlement Balance
    participant Engine as Credit Engine
    end

    API->>Conn: Request entitlement value
    Conn->>Balance: GetEntitlementBalance(owner, meter, at)
    Balance->>Engine: BalanceConnector.GetBalanceAt(owner, snap, at)
    Engine->>Engine: RunParams.Clone()
    Engine->>Engine: Calculate TotalAvailableGrantAmount()
    Engine-->>Balance: RunResult{..., RunParams}
    Balance->>Conn: EntitlementBalance{..., TotalAvailableGrantAmount}
    Conn->>API: Map to MeteredEntitlementValue / EntitlementValue (includes TotalAvailableGrantAmount)
    API->>Client: JSON response (totalAvailableGrantAmount)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review Clone() implementation for correct deep-copy semantics (grants, resets timeline).
  • Verify numeric precision/rounding in TotalAvailableGrantAmount() (alpacadecimal usage).
  • Confirm no external packages relied on the now-unexported GetBalanceSinceSnapshot.

Possibly related PRs

Suggested labels

area/api, release-note/breaking-change

Suggested reviewers

  • tothandras
  • chrisgacsal
  • turip

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change—exposing a new TotalAvailableGrantAmount field across the entitlement system. It's specific, clear, and directly summarizes the primary objective.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/entitlements/tag

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.

chrisgacsal
chrisgacsal previously approved these changes Oct 29, 2025
Copy link
Contributor

@chrisgacsal chrisgacsal left a comment

Choose a reason for hiding this comment

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

Awesome! :shipit:

Copy link
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: 2

🧹 Nitpick comments (2)
api/spec/src/entitlements/subjects.tsp (1)

233-238: Consider clarifying "active" grants in the documentation.

The documentation mentions "grant amounts that are active at the time of the query" but doesn't specify what makes a grant active. Consider adding a brief note about whether this includes grants that haven't reached their effective date yet, or have expired, etc. This will help API consumers understand the field better.

e2e/e2e_test.go (1)

1104-1108: Test validates the new field, but could be more thorough.

The test correctly expects TotalAvailableGrantAmount to be 100.0, matching the grant created earlier. However, consider adding test cases for:

  • Multiple grants (to verify summation)
  • Zero grants (to verify the field handles empty cases)
  • Expired or inactive grants (to verify only "active" grants are counted)

This would give more confidence that the calculation logic is robust.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58c348d and 4d69745.

⛔ Files ignored due to path filters (5)
  • api/client/go/client.gen.go is excluded by !api/client/**
  • api/client/javascript/src/client/schemas.ts is excluded by !api/client/**
  • api/client/python/openmeter/_generated/models/_models.py is excluded by !**/_generated/**, !api/client/**
  • api/openapi.cloud.yaml is excluded by !**/openapi.cloud.yaml
  • api/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (9)
  • api/spec/src/entitlements/subjects.tsp (1 hunks)
  • e2e/e2e_test.go (1 hunks)
  • openmeter/credit/balance.go (2 hunks)
  • openmeter/credit/engine/engine.go (2 hunks)
  • openmeter/credit/engine/run.go (2 hunks)
  • openmeter/entitlement/driver/parser.go (1 hunks)
  • openmeter/entitlement/metered/balance.go (2 hunks)
  • openmeter/entitlement/metered/connector.go (2 hunks)
  • openmeter/entitlement/snapshot/event.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/credit/engine/run.go
  • openmeter/entitlement/snapshot/event.go
  • openmeter/entitlement/metered/balance.go
  • openmeter/entitlement/metered/connector.go
  • openmeter/entitlement/driver/parser.go
  • e2e/e2e_test.go
  • openmeter/credit/engine/engine.go
  • openmeter/credit/balance.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.
When appropriate, recommend e2e tests for critical changes.

Files:

  • e2e/e2e_test.go
**/*.tsp

⚙️ CodeRabbit configuration file

**/*.tsp: Review the TypeSpec code for conformity with TypeSpec best practices. When recommending changes also consider the fact that multiple codegeneration toolchains depend on the TypeSpec code, each of which have their idiosyncrasies and bugs.

The declared API should be accurate, in parity with the actual implementation, and easy to understand for the user.

Files:

  • api/spec/src/entitlements/subjects.tsp
🧬 Code graph analysis (6)
openmeter/credit/engine/run.go (2)
openmeter/credit/balance/balance.go (1)
  • Snapshot (80-85)
openmeter/credit/engine/engine.go (1)
  • RunParams (16-30)
openmeter/entitlement/metered/balance.go (2)
openmeter/credit/balance/balance.go (1)
  • Snapshot (80-85)
openmeter/credit/engine/engine.go (1)
  • RunParams (16-30)
openmeter/entitlement/metered/connector.go (2)
openmeter/ent/db/balancesnapshot/where.go (2)
  • Balance (84-86)
  • Overage (89-91)
openmeter/ent/db/entitlement/where.go (1)
  • IsSoftLimit (140-142)
e2e/e2e_test.go (1)
pkg/convert/ptr.go (1)
  • ToPointer (10-12)
openmeter/credit/engine/engine.go (6)
openmeter/credit/grant/grant.go (1)
  • Grant (14-60)
pkg/timeutil/timeline.go (1)
  • NewSimpleTimeline (11-18)
openmeter/meter/meter.go (1)
  • Meter (139-148)
openmeter/credit/grant/owner_connector.go (1)
  • ResetBehavior (32-34)
openmeter/credit/balance/balance.go (1)
  • Snapshot (80-85)
openmeter/credit/engine/history.go (1)
  • GrantBurnDownHistory (93-96)
openmeter/credit/balance.go (3)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/credit/balance/balance.go (1)
  • Snapshot (80-85)
openmeter/credit/engine/engine.go (1)
  • RunResult (58-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Migration Checks
  • GitHub Check: Code Generators
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (10)
openmeter/credit/balance.go (1)

43-44: Nice encapsulation improvement!

Making getBalanceSinceSnapshot private is a solid move since it was never exposed through the BalanceConnector interface. This keeps implementation details internal while the public API remains clean.

openmeter/entitlement/snapshot/event.go (1)

55-56: LGTM! Field addition is clean and consistent.

The new TotalAvailableGrantAmount field aligns perfectly with the API spec, using the correct Go patterns (pointer for optional field, omitempty JSON tag).

openmeter/credit/engine/run.go (1)

17-17: Nice defensive programming with the param cloning!

Cloning the input params and returning them in the result ensures the RunResult carries an immutable snapshot of the parameters used for the calculation. This makes the result self-contained and easier to reason about.

Also applies to: 95-97

openmeter/entitlement/driver/parser.go (1)

160-164: Clean mapping implementation!

The field mapping correctly threads TotalAvailableGrantAmount from the internal MeteredEntitlementValue to the API response. Only populating it for metered entitlements makes sense since grants are specific to that entitlement type.

openmeter/entitlement/metered/connector.go (1)

49-54: Straightforward field addition, well integrated!

The TotalAvailableGrantAmount field is cleanly added to MeteredEntitlementValue and correctly populated from the balance. The struct fields are nicely organized alongside other balance-related properties.

Also applies to: 132-137

openmeter/credit/engine/engine.go (2)

32-38: Clean implementation of grant amount summation!

Using alpacadecimal for the intermediate calculation helps maintain precision before converting back to float64. This aligns with the established pattern in the codebase and the acknowledgment on line 75 that float64 limitations are a known concern.


63-64: RunParams field addition makes results more informative!

Embedding the exact parameters used to produce the result makes RunResult self-contained and easier to debug. Nice enhancement to the API.

openmeter/entitlement/metered/balance.go (3)

22-29: Nice addition!

The new TotalAvailableGrantAmount field is properly defined with the correct JSON tag. The structure looks clean and consistent with the existing fields.


83-90: Looking good!

The new field is properly populated from res.RunParams.TotalAvailableGrantAmount(), and all the existing fields are correctly mapped. The balance calculation flow makes sense.


78-78: Method change verified—behavior is equivalent.

The refactoring from GetBalanceSinceSnapshot to GetBalanceAt is solid. The BalanceConnector interface only exposes GetBalanceAt (line 31 in openmeter/credit/balance.go), and it's explicitly designed to "minimize execution cost by calculating from the latest valid snapshot." That's exactly what the old internal getBalanceSinceSnapshot helper was doing, so the semantics are identical. The error message "failed to get balance since snapshot" remains accurate since GetBalanceAt handles that optimization internally.

Comment on lines +40 to +56
func (p RunParams) Clone() RunParams {
grants := make([]grant.Grant, len(p.Grants))
copy(grants, p.Grants)

resets := timeutil.NewSimpleTimeline(p.Resets.GetTimes())

res := RunParams{
Meter: p.Meter,
Grants: grants,
Until: p.Until,
StartingSnapshot: p.StartingSnapshot,
ResetBehavior: p.ResetBehavior,
Resets: resets,
}

return res
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clone method has a shallow copy issue with Meter.GroupBy.

The Clone() method copies the Meter field by value, but Meter.GroupBy is a map[string]string (reference type). If the original Meter.GroupBy is mutated after cloning, it will affect the cloned RunParams too. Consider deep copying the map:

 func (p RunParams) Clone() RunParams {
 	grants := make([]grant.Grant, len(p.Grants))
 	copy(grants, p.Grants)
 
 	resets := timeutil.NewSimpleTimeline(p.Resets.GetTimes())
+
+	// Deep copy Meter.GroupBy map
+	meterCopy := p.Meter
+	if p.Meter.GroupBy != nil {
+		meterCopy.GroupBy = make(map[string]string, len(p.Meter.GroupBy))
+		for k, v := range p.Meter.GroupBy {
+			meterCopy.GroupBy[k] = v
+		}
+	}
 
 	res := RunParams{
-		Meter:            p.Meter,
+		Meter:            meterCopy,
 		Grants:           grants,
 		Until:            p.Until,
 		StartingSnapshot: p.StartingSnapshot,
 		ResetBehavior:    p.ResetBehavior,
 		Resets:           resets,
 	}
 
 	return res
 }
🤖 Prompt for AI Agents
In openmeter/credit/engine/engine.go around lines 40 to 56, the Clone method
performs a shallow copy of p.Meter so its GroupBy map (map[string]string) is
shared between original and clone; modify Clone to deep-copy the Meter by
creating a new Meter value, allocate a new map for its GroupBy, and copy all
key/value pairs from p.Meter.GroupBy into that new map before assigning the new
Meter to res.Meter so mutations to the original Meter.GroupBy do not affect the
clone.

res, err := e.balanceConnector.GetBalanceSinceSnapshot(ctx, nsOwner, lastSnap, at)
res, err := e.balanceConnector.GetBalanceAt(ctx, nsOwner, at)
if err != nil {
return nil, fmt.Errorf("failed to get balance since snapshot: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the error message to match the method call.

The error message still references "balance since snapshot" but the code now calls GetBalanceAt on line 78. Let's keep them in sync for easier debugging.

Apply this diff:

-		return nil, fmt.Errorf("failed to get balance since snapshot: %w", err)
+		return nil, fmt.Errorf("failed to get balance at time: %w", err)
📝 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
return nil, fmt.Errorf("failed to get balance since snapshot: %w", err)
return nil, fmt.Errorf("failed to get balance at time: %w", err)
🤖 Prompt for AI Agents
In openmeter/entitlement/metered/balance.go around line 80, the error message
still says "failed to get balance since snapshot" while the code calls
GetBalanceAt on line 78; update the fmt.Errorf message to reference GetBalanceAt
(for example "failed to get balance at snapshot" or "failed to get balance at
specified time") so the log matches the method being called and aids debugging.

Copy link
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: 0

♻️ Duplicate comments (1)
openmeter/entitlement/metered/balance.go (1)

85-85: Update error message to match method call.

The error message still references "balance since snapshot" but the code now calls GetBalanceAt on line 83. Let's sync them up for easier debugging.

Apply this diff:

-		return nil, fmt.Errorf("failed to get balance since snapshot: %w", err)
+		return nil, fmt.Errorf("failed to get balance at time: %w", err)
🧹 Nitpick comments (1)
openmeter/entitlement/metered/balance.go (1)

29-29: Consider adding documentation for new field.

The new TotalAvailableGrantAmount field doesn't have a doc comment. Adding a brief comment would help clarify what this represents (e.g., "total amount of all active grants for this entitlement").

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d69745 and 2ada658.

⛔ Files ignored due to path filters (5)
  • api/client/go/client.gen.go is excluded by !api/client/**
  • api/client/javascript/src/client/schemas.ts is excluded by !api/client/**
  • api/client/python/openmeter/_generated/models/_models.py is excluded by !**/_generated/**, !api/client/**
  • api/openapi.cloud.yaml is excluded by !**/openapi.cloud.yaml
  • api/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (9)
  • api/spec/src/entitlements/subjects.tsp (1 hunks)
  • e2e/e2e_test.go (1 hunks)
  • openmeter/credit/balance.go (2 hunks)
  • openmeter/credit/engine/engine.go (2 hunks)
  • openmeter/credit/engine/run.go (2 hunks)
  • openmeter/entitlement/driver/parser.go (1 hunks)
  • openmeter/entitlement/metered/balance.go (2 hunks)
  • openmeter/entitlement/metered/connector.go (2 hunks)
  • openmeter/entitlement/snapshot/event.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • openmeter/entitlement/snapshot/event.go
  • openmeter/credit/engine/engine.go
  • openmeter/entitlement/metered/connector.go
  • api/spec/src/entitlements/subjects.tsp
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/credit/engine/run.go
  • openmeter/credit/balance.go
  • openmeter/entitlement/driver/parser.go
  • e2e/e2e_test.go
  • openmeter/entitlement/metered/balance.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.
When appropriate, recommend e2e tests for critical changes.

Files:

  • e2e/e2e_test.go
🧬 Code graph analysis (4)
openmeter/credit/engine/run.go (1)
openmeter/credit/engine/engine.go (1)
  • RunParams (16-30)
openmeter/credit/balance.go (3)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/credit/balance/balance.go (1)
  • Snapshot (80-85)
openmeter/credit/engine/engine.go (1)
  • RunResult (58-65)
e2e/e2e_test.go (1)
pkg/convert/ptr.go (1)
  • ToPointer (10-12)
openmeter/entitlement/metered/balance.go (2)
openmeter/credit/balance/balance.go (1)
  • Snapshot (80-85)
openmeter/credit/engine/engine.go (1)
  • RunParams (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Artifacts / Benthos Collector Container image
  • GitHub Check: Test
  • GitHub Check: Build
  • GitHub Check: Migration Checks
  • GitHub Check: Code Generators
  • GitHub Check: Lint
  • GitHub Check: Repository Scan
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
openmeter/credit/balance.go (1)

43-144: LGTM! Clean encapsulation improvement.

Nice refactor making getBalanceSinceSnapshot unexported. The change is consistent across both the definition and the call site, and the comment is properly updated too. This keeps the internal balance calculation details private to the package while still exposing the public GetBalanceAt interface.

openmeter/entitlement/driver/parser.go (1)

160-164: LGTM! Correct field mapping.

The TotalAvailableGrantAmount field is properly mapped from MeteredEntitlementValue to the API response. Makes sense that only metered entitlements have this field since static and boolean entitlements don't use grants.

e2e/e2e_test.go (1)

1270-1274: LGTM! Test correctly validates the new field.

The test expectation for TotalAvailableGrantAmount: convert.ToPointer(100.0) matches the grant amount created earlier in the test (line 1117). The values all make sense together: 100.0 total granted, 1.0 used, 99.0 remaining balance.

openmeter/entitlement/metered/balance.go (1)

88-95: LGTM! New field properly populated.

The TotalAvailableGrantAmount is correctly populated from res.RunParams.TotalAvailableGrantAmount() and included in the returned balance. The change aligns well with the PR objective to expose this information.

openmeter/credit/engine/run.go (1)

17-17: Looking at the Clone() implementation, I can see it's doing a shallow copy of the grants slice and creating a new SimpleTimeline. The main unknown is whether the GetTimes() call in the SimpleTimeline creation is expensive. Let me check that real quick:

Let me search the codebase directly for the SimpleTimeline implementation:

Clone() implementation is appropriate for the use case; verify if capturing params in RunResult is necessary.

The Clone() method does a shallow copy of the grants slice and creates a new SimpleTimeline—both necessary operations to ensure the cloned params are independent. The concern about performance is worth flagging, but the implementation itself is sound: it uses efficient Go patterns (make + copy for the slice, and creates a fresh SimpleTimeline from times).

The real question is whether capturing the params in RunResult justifies the clone on every invocation. If this is needed for auditing or debugging (seems likely given it's stored in the result), then the allocation is warranted. If it's not strictly necessary, you could defer the clone until actually needed or explore using pointers instead. That said, the current approach is a reasonable trade-off for correctness and debuggability on a balance calculation path.

@GAlexIHU GAlexIHU merged commit c0ef47c into main Nov 12, 2025
27 checks passed
@GAlexIHU GAlexIHU deleted the feat/entitlements/tag branch November 12, 2025 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants