Skip to content

feat(api): v3 feature handlers#3978

Closed
hekike wants to merge 9 commits intomainfrom
feat/feature-handlers
Closed

feat(api): v3 feature handlers#3978
hekike wants to merge 9 commits intomainfrom
feat/feature-handlers

Conversation

@hekike
Copy link
Contributor

@hekike hekike commented Mar 19, 2026

Changes

  • Fix: Feature has no description field
  • Fix: Reference meter via ID
  • Feat: Add unit cost v3 API
  • Feat: Implement feature handlers

Todo

  • Validate v3 feature meter filters and if mapping works in all cases

V3 Feature API - Test Results

# Operation Test Case Expected Actual Status
1 GET /api/v3/openmeter/features List features (paginated) 200 200 Pass
2 GET /api/v3/openmeter/features/:id Get feature by ID (LLM unit cost) 200 200 Pass
3 POST /api/v3/openmeter/features Create with manual unit cost ("amount": "0.005") 201 201 Pass
4 POST /api/v3/openmeter/features Create with LLM unit cost (property refs) 201 201 Pass
5 POST /api/v3/openmeter/features Create without unit cost 201 201 Pass
6 POST /api/v3/openmeter/features Create with LLM static values + meter filters 201 201 Pass
7 GET /api/v3/openmeter/features List all (5 features, mixed unit cost types) 200 200 Pass
8 GET /api/v3/openmeter/features/:id Get feature with manual cost 200 200 Pass
9 DELETE /api/v3/openmeter/features/:id Delete (archive) feature 204 204 Pass
10 GET /api/v3/openmeter/features/:id Get deleted feature 404 404 Pass
11 POST /api/v3/openmeter/features Invalid unit cost type ("type": "invalid") 400 400 Pass
12 POST /api/v3/openmeter/features Missing required fields (empty body) 400 400 Pass
13 POST /api/v3/openmeter/features Duplicate key 409 409 Pass
14 GET /api/v3/openmeter/features/:id Non-existent feature ID 404 404 Pass
15 POST /api/v3/openmeter/features Non-existent meter key 404 404 Pass

Summary by CodeRabbit

  • New Features
    • Optional unit cost on features with two modes:
      • Manual: fixed per‑unit USD
      • LLM: per‑token pricing resolved dynamically; pricing enriched on feature responses when available
  • API / UX
    • Feature CRUD endpoints wired: list, get, create, delete with validation and mapped error responses
    • Feature payloads: description removed; meter now referenced by required ID plus optional group‑by filters; unit_cost exposed in API
  • Tests
    • Added conversion and pricing enrichment tests for unit cost handling

@hekike hekike requested a review from a team as a code owner March 19, 2026 20:33
@hekike hekike added the release-note/feature Release note: Exciting New Features label Mar 19, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added per-feature unit_cost (manual or LLM) and pricing models, changed Feature schema (removed inherited description, adjusted meter reference shape), generated Go API types, added conversion/enrichment for LLM pricing, implemented List/Get/Create/Delete handlers, tests, and wired handlers into the server.

Changes

Cohort / File(s) Summary
TypeSpec / Feature Schema
api/spec/packages/aip/src/features/feature.tsp, api/spec/packages/aip/src/features/index.tsp, api/spec/packages/aip/src/features/unitcost.tsp
Removed inherited description, changed meter reference to explicit id + optional filters, added unit_cost discriminated union (manual, llm) and LLM pricing models.
Generated API Types
api/v3/api.gen.go
Generated Go enums/structs and JSON-union helpers for FeatureUnitCost (manual/llm) and pricing; added UnitCost to Feature payload; removed Description; changed meter id/key shape; updated swagger.
Handlers & Server Wiring
api/v3/handlers/features/handler.go, .../list.go, .../get.go, .../create.go, .../delete.go, api/v3/server/routes.go, api/v3/server/server.go
Added features handler interface and constructor, implemented List/Get/Create/Delete HTTP handlers with namespace resolution and pagination, and wired the handlers into server routes and Server struct.
Conversion, Enrichment & Errors
api/v3/handlers/features/convert.go, api/v3/handlers/features/error_encoder.go, api/v3/handlers/features/convert_test.go
Added domain↔API conversions for features, meter filter mapping, labels↔metadata, unit-cost conversion (manual/LLM), LLM pricing resolution/enrichment, error encoder, and comprehensive conversion/enrichment tests.

Sequence Diagram

sequenceDiagram
    participant Client
    participant HTTP as "Features Handler"
    participant Conv as "Converter"
    participant Conn as "Feature Connector"
    participant LLM as "LLM Cost Service"
    participant Resp as "API Response"

    Client->>HTTP: GET /features/:id
    HTTP->>HTTP: resolve namespace
    HTTP->>Conn: GetFeature(namespace, idOrKey)
    Conn-->>HTTP: domain Feature (may include UnitCost)
    HTTP->>Conv: convertFeatureToAPI(domain Feature)
    Conv-->>HTTP: api.Feature

    alt unit_cost.type == "llm"
        HTTP->>LLM: ResolvePrice(provider, model)
        LLM-->>HTTP: FeatureLLMUnitCostPricing
        HTTP->>Conv: enrichFeatureResponseWithPricing(api.Feature, pricing)
        Conv-->>HTTP: enriched api.Feature
    end

    HTTP->>Resp: write JSON 200 OK
    Resp-->>Client: HTTP 200 + feature payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

area/product-catalog

Suggested reviewers

  • tothandras
  • chrisgacsal
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(api): v3 feature handlers' directly and clearly summarizes the main change—implementing feature API handlers for v3—matching the primary deliverable of this changeset.
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
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/feature-handlers
📝 Coding Plan
  • Generate coding plan for human review comments

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.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/handlers/features/convert.go`:
- Around line 19-28: The API is advertising a Description field but the
converters drop it: when building api.Feature you set Description: nil and
CreateFeatureRequest.description isn't propagated into the internal feature
model. Fix by wiring the description through both directions: in the request ->
internal conversion copy CreateFeatureRequest.Description into the internal
feature struct, and in this response construction set api.Feature.Description to
the internal value (e.g., Description: f.Description). Update any other similar
blocks (the other occurrence around lines 56-62) to mirror this change so
create/get/list round-trip descriptions.
- Around line 30-43: The handler only sets resp.Meter.Key from f.MeterSlug and
drops any provided meter ID; update the mapping to also set resp.Meter.Id when
f.MeterSlug contains an ID (i.e., copy the ID into resp.Meter.Id alongside Key
and Filters) so create requests that reference a meter by id are preserved and
reads can return meter.id; apply the same fix to the second similar block (the
one around lines 64-72) and ensure the source struct (f.MeterSlug) and helper
convertFiltersToAPI usage remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3571384f-9a2f-414d-bba8-f28105716559

📥 Commits

Reviewing files that changed from the base of the PR and between 4971c89 and cd06843.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (13)
  • api/spec/packages/aip/src/features/feature.tsp
  • api/spec/packages/aip/src/features/index.tsp
  • api/spec/packages/aip/src/features/unitcost.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/features/convert.go
  • api/v3/handlers/features/create.go
  • api/v3/handlers/features/delete.go
  • api/v3/handlers/features/error_encoder.go
  • api/v3/handlers/features/get.go
  • api/v3/handlers/features/handler.go
  • api/v3/handlers/features/list.go
  • api/v3/server/routes.go
  • api/v3/server/server.go

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: 1

🧹 Nitpick comments (2)
api/v3/handlers/features/convert_test.go (2)

497-520: Filter conversion coverage is a bit narrow here.

This test set checks Eq/In/Ne/Exists but skips Nin (which is mapped) and doesn’t assert behavior for unsupported operators, so silent conversion loss can slip through unnoticed.

As per coding guidelines "**/*_test.go: Make sure the tests are comprehensive and cover the changes."

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

In `@api/v3/handlers/features/convert_test.go` around lines 497 - 520, The tests
for TestConvertFilterStringToAPIMapItem are missing coverage for the Nin
operator and for behavior when unsupported operators are provided; update the
test to add a subtest that constructs filter.FilterString{Nin:
&[]string{"x","y"}} and assert the returned result.Nin matches, and add another
subtest that passes an unsupported/empty operator (or a struct with an
unexpected field) and asserts the conversion either yields nil/zero values or
returns the expected error/value according to convertFilterStringToAPIMapItem’s
contract; reference convertFilterStringToAPIMapItem and
TestConvertFilterStringToAPIMapItem when adding these assertions so silent
conversion loss is caught.

97-150: Please add error-path tests for convertUnitCostFromAPI.

This block only verifies happy paths. We should also cover malformed/unknown unit-cost payloads so parser/union regressions are caught early.

Suggested test additions
 func TestConvertUnitCostFromAPI(t *testing.T) {
+	t.Run("unknown discriminator returns error", func(t *testing.T) {
+		var apiUC api.FeatureUnitCost
+		err := apiUC.FromFeatureUnknownUnitCostType(map[string]any{"type": "unknown"})
+		require.NoError(t, err)
+
+		_, err = convertUnitCostFromAPI(&apiUC)
+		require.Error(t, err)
+	})
+
+	t.Run("manual unit cost with invalid amount returns error", func(t *testing.T) {
+		var apiUC api.FeatureUnitCost
+		err := apiUC.FromFeatureManualUnitCost(api.FeatureManualUnitCost{
+			Amount: "not-a-number",
+		})
+		require.NoError(t, err)
+
+		_, err = convertUnitCostFromAPI(&apiUC)
+		require.Error(t, err)
+	})
+
 	t.Run("manual unit cost", func(t *testing.T) {

As per coding guidelines "**/*_test.go: Make sure the tests are comprehensive and cover the changes."

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

In `@api/v3/handlers/features/convert_test.go` around lines 97 - 150, Add negative
tests in convert_test.go that call convertUnitCostFromAPI and assert errors for
malformed/unknown payloads: (1) a subtest that passes an empty
api.FeatureUnitCost (no union set) and expects a non-nil error; (2) a subtest
that uses apiUC.FromFeatureManualUnitCost with Amount set to an invalid string
(e.g. "not-a-number") and expects convertUnitCostFromAPI to return an error; (3)
a subtest that constructs an api.FeatureLLMUnitCost with both static fields and
corresponding Property fields set (e.g. Provider and ProviderProperty) and
expects convertUnitCostFromAPI to error on the conflicting LLM union. Reference
convertUnitCostFromAPI and the
api.FeatureUnitCost/FromFeatureManualUnitCost/FromFeatureLLMUnitCost helpers to
locate where to add these tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/handlers/features/convert_test.go`:
- Around line 417-424: The test currently ignores errors from
uc.FromFeatureLLMUnitCost and resp.UnitCost.AsFeatureLLMUnitCost which can hide
setup/decoding failures; update the subtest to check and fail on those errors
(i.e., capture the returned error from FeatureUnitCost.FromFeatureLLMUnitCost
and from AsFeatureLLMUnitCost and call t.Fatalf or require.NoError if non-nil)
before calling enrichFeatureResponseWithPricing and asserting llm.Pricing,
referencing the FeatureUnitCost.FromFeatureLLMUnitCost, resp.UnitCost,
enrichFeatureResponseWithPricing, and resp.UnitCost.AsFeatureLLMUnitCost
symbols.

---

Nitpick comments:
In `@api/v3/handlers/features/convert_test.go`:
- Around line 497-520: The tests for TestConvertFilterStringToAPIMapItem are
missing coverage for the Nin operator and for behavior when unsupported
operators are provided; update the test to add a subtest that constructs
filter.FilterString{Nin: &[]string{"x","y"}} and assert the returned result.Nin
matches, and add another subtest that passes an unsupported/empty operator (or a
struct with an unexpected field) and asserts the conversion either yields
nil/zero values or returns the expected error/value according to
convertFilterStringToAPIMapItem’s contract; reference
convertFilterStringToAPIMapItem and TestConvertFilterStringToAPIMapItem when
adding these assertions so silent conversion loss is caught.
- Around line 97-150: Add negative tests in convert_test.go that call
convertUnitCostFromAPI and assert errors for malformed/unknown payloads: (1) a
subtest that passes an empty api.FeatureUnitCost (no union set) and expects a
non-nil error; (2) a subtest that uses apiUC.FromFeatureManualUnitCost with
Amount set to an invalid string (e.g. "not-a-number") and expects
convertUnitCostFromAPI to return an error; (3) a subtest that constructs an
api.FeatureLLMUnitCost with both static fields and corresponding Property fields
set (e.g. Provider and ProviderProperty) and expects convertUnitCostFromAPI to
error on the conflicting LLM union. Reference convertUnitCostFromAPI and the
api.FeatureUnitCost/FromFeatureManualUnitCost/FromFeatureLLMUnitCost helpers to
locate where to add these tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 095598ad-5ba3-4c04-b5a5-24278de44355

📥 Commits

Reviewing files that changed from the base of the PR and between cd06843 and 71427cf.

📒 Files selected for processing (2)
  • api/v3/handlers/features/convert.go
  • api/v3/handlers/features/convert_test.go
✅ Files skipped from review due to trivial changes (1)
  • api/v3/handlers/features/convert.go

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 (1)
api/v3/handlers/features/convert_test.go (1)

286-356: Please cover the LLM create-request path too.

convertCreateRequestToDomain only gets manual unit_cost coverage here, even though this helper is what CreateFeature uses for the new llm request shapes. I’d add at least one property-ref case and one static-value case so both supported forms stay protected in CI.

As per coding guidelines "Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests."

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

In `@api/v3/handlers/features/convert_test.go` around lines 286 - 356, Add tests
exercising the LLM unit_cost conversion in convertCreateRequestToDomain (the
same helper used by CreateFeature): add one subtest where
api.CreateFeatureRequest contains an LLM unit_cost using a property-ref form and
one subtest with a static-value LLM form, then assert the returned
feature.UnitCost.Type and the parsed values (e.g., that result.UnitCost.Type
equals the LLM-specific enum and the property reference or static amount is
populated correctly). Locate convertCreateRequestToDomain and the
TestConvertCreateRequestToDomain function to add the new t.Run cases and reuse
api.FeatureUnitCost/FromFeature... helpers to construct the request shapes so CI
covers both supported LLM unit_cost variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/api.gen.go`:
- Around line 2177-2180: The generated comment for the UnitCost field is too
narrow; update the source API spec (the schema/description for FeatureUnitCost /
the UnitCost field) to state that "llm" pricing supports both dynamic
meter-based lookups and static forms using provider, model, and token_type, then
regenerate the API client to update api.gen.go (do not edit the generated file
directly). Ensure you change the FeatureUnitCost/UnitCost description in the
authoritative spec and re-run the codegen so all occurrences (the other
generated doc strings for UnitCost/FeatureUnitCost) are updated.

In `@api/v3/handlers/features/convert_test.go`:
- Around line 358-427: Add a test case that ensures
enrichFeatureResponseWithPricing is a no-op for non-LLM/manual unit costs:
create a Feature with a UnitCost that represents a manual/non-LLM pricing
variant (not an LLM unit cost), record its value, call
enrichFeatureResponseWithPricing(resp, somePricing) and assert the UnitCost
remains unchanged (e.g., deep-equal to the original or that converting to an LLM
unit cost does not expose Pricing). Update the helper
enrichFeatureResponseWithPricing to first check the unit-cost discriminator
(only proceed when the unit cost is an LLM type) before mutating resp.UnitCost
so non-LLM/manual unit costs are left untouched.

---

Nitpick comments:
In `@api/v3/handlers/features/convert_test.go`:
- Around line 286-356: Add tests exercising the LLM unit_cost conversion in
convertCreateRequestToDomain (the same helper used by CreateFeature): add one
subtest where api.CreateFeatureRequest contains an LLM unit_cost using a
property-ref form and one subtest with a static-value LLM form, then assert the
returned feature.UnitCost.Type and the parsed values (e.g., that
result.UnitCost.Type equals the LLM-specific enum and the property reference or
static amount is populated correctly). Locate convertCreateRequestToDomain and
the TestConvertCreateRequestToDomain function to add the new t.Run cases and
reuse api.FeatureUnitCost/FromFeature... helpers to construct the request shapes
so CI covers both supported LLM unit_cost variants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ce476628-bf4b-45e2-9619-5f99225a3ae8

📥 Commits

Reviewing files that changed from the base of the PR and between 71427cf and ec91df1.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (4)
  • api/spec/packages/aip/src/features/feature.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/features/convert.go
  • api/v3/handlers/features/convert_test.go
✅ Files skipped from review due to trivial changes (1)
  • api/v3/handlers/features/convert.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/spec/packages/aip/src/features/feature.tsp

…fixes

- Reference meters by required ID instead of key in v3 Feature API
- Remove unsupported description field from Feature model
- Resolve meter ID to slug in CreateFeature handler via meter service
- Fix unnecessary type conversions flagged by linter
- Fix test to check error returns instead of discarding them

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The FeatureLLMUnitCost and pricing field descriptions implied that only
meter group-by properties were supported. Updated to accurately describe
that both static values and property references work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

♻️ Duplicate comments (1)
api/v3/api.gen.go (1)

2177-2180: ⚠️ Potential issue | 🟡 Minor

Please widen the LLM unit-cost docs before regenerating.

These comments still read like llm pricing only works through meter-property lookups, but the schema now also allows static provider, model, and token_type values. Please fix the source TypeSpec/OpenAPI description and regenerate so the generated Go docs and embedded spec stay aligned. As per coding guidelines "Never manually edit generated files marked with '// Code generated by X, DO NOT EDIT.' headers"

Also applies to: 2309-2312, 2356-2358

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

In `@api/v3/api.gen.go` around lines 2177 - 2180, The comment on the generated
UnitCost docs is outdated: update the source TypeSpec/OpenAPI description for
the FeatureUnitCost/UnitCost property to reflect that "llm" pricing can derive
cost either from meter-property lookups or from static provider/model/token_type
values (and not only meter lookups), then regenerate the OpenAPI spec and re-run
codegen so api.gen.go's UnitCost comment (for FeatureUnitCost) and the embedded
spec remain synchronized; locate the TypeSpec that defines
UnitCost/FeatureUnitCost and expand its description to mention both
meter-property based lookup and the optional static provider/model/token_type
fields before regenerating.
🧹 Nitpick comments (3)
api/v3/handlers/features/create.go (1)

35-46: Good meter resolution flow, but the error from GetMeterByIDOrSlug could use context.

The meter ID-to-slug resolution is correctly implemented. One small thing: when GetMeterByIDOrSlug fails, the raw error is returned. While the error encoder will handle MeterNotFoundError, wrapping it with context (e.g., "failed to resolve meter") would make debugging easier when other error types occur.

Optional: Add context to meter resolution error
 			m, err := h.meterService.GetMeterByIDOrSlug(ctx, meter.GetMeterInput{
 				Namespace: ns,
 				IDOrSlug:  body.Meter.Id,
 			})
 			if err != nil {
-				return CreateFeatureRequest{}, err
+				return CreateFeatureRequest{}, fmt.Errorf("failed to resolve meter: %w", err)
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/features/create.go` around lines 35 - 46, When
GetMeterByIDOrSlug (via h.meterService.GetMeterByIDOrSlug) returns an error,
wrap that error with contextual text before returning so callers can see "failed
to resolve meter" plus the original error; update the error return in the meter
resolution block (the branch that sets meterSlug) to wrap the err (e.g., using
fmt.Errorf("failed to resolve meter %s: %w", body.Meter.Id, err) or errors.Wrap)
and return the wrapped error instead of the raw err.
api/v3/handlers/features/convert.go (2)

196-198: Silently ignoring the error from FromFeatureLLMUnitCost.

If FromFeatureLLMUnitCost fails, the response's UnitCost remains in an inconsistent state (the pricing was built but not applied). At minimum, consider logging this error for debugging purposes.

Optional: Log conversion error
 	llmCost.Pricing = &apiPricing
-	_ = resp.UnitCost.FromFeatureLLMUnitCost(llmCost)
+	if err := resp.UnitCost.FromFeatureLLMUnitCost(llmCost); err != nil {
+		// Log but don't fail - pricing enrichment is best-effort
+		slog.Debug("failed to enrich feature with pricing", "error", err)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/features/convert.go` around lines 196 - 198, The call to
FromFeatureLLMUnitCost is currently ignoring its error which can leave
resp.UnitCost inconsistent after llmCost.Pricing = &apiPricing; change the code
to capture the returned error from
resp.UnitCost.FromFeatureLLMUnitCost(llmCost), log that error with context
(including the feature/price info and any request identifiers) and handle it
(return the error or set a safe fallback UnitCost) instead of discarding it;
update the call site where FromFeatureLLMUnitCost and resp.UnitCost are used to
handle the error path explicitly.

200-234: Silent error suppression in resolveLLMPricing hides potential issues.

When svc.ResolvePrice fails (line 229), the function returns nil without any indication of why. This is fine for "not found" cases, but database errors or validation failures are also swallowed. Consider logging at debug level to aid troubleshooting, especially since the context snippet shows ResolvePrice can return both not-found errors and database errors.

Optional: Add debug logging for pricing resolution failures
 	price, err := svc.ResolvePrice(ctx, llmcost.ResolvePriceInput{
 		Namespace: feat.Namespace,
 		Provider:  llmcost.Provider(provider),
 		ModelID:   model,
 	})
 	if err != nil {
+		// Log at debug level - pricing lookup failures are not critical
+		slog.DebugContext(ctx, "failed to resolve LLM pricing",
+			"provider", provider,
+			"model", model,
+			"error", err,
+		)
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/features/convert.go` around lines 200 - 234,
resolveLLMPricing currently swallows errors from svc.ResolvePrice; when
ResolvePrice returns an error you should log the failure (at debug level) with
the error and contextual info (feat.Namespace, provider, model) before returning
nil. Locate resolveLLMPricing and the svc.ResolvePrice call and add a debug log
statement that extracts/uses the request context logger (e.g. logger from ctx or
the project's logging helper) to emit a message like "failed to resolve LLM
price" including err, feat.Namespace, provider and model, then keep returning
nil for not-found flows.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@api/v3/api.gen.go`:
- Around line 2177-2180: The comment on the generated UnitCost docs is outdated:
update the source TypeSpec/OpenAPI description for the FeatureUnitCost/UnitCost
property to reflect that "llm" pricing can derive cost either from
meter-property lookups or from static provider/model/token_type values (and not
only meter lookups), then regenerate the OpenAPI spec and re-run codegen so
api.gen.go's UnitCost comment (for FeatureUnitCost) and the embedded spec remain
synchronized; locate the TypeSpec that defines UnitCost/FeatureUnitCost and
expand its description to mention both meter-property based lookup and the
optional static provider/model/token_type fields before regenerating.

---

Nitpick comments:
In `@api/v3/handlers/features/convert.go`:
- Around line 196-198: The call to FromFeatureLLMUnitCost is currently ignoring
its error which can leave resp.UnitCost inconsistent after llmCost.Pricing =
&apiPricing; change the code to capture the returned error from
resp.UnitCost.FromFeatureLLMUnitCost(llmCost), log that error with context
(including the feature/price info and any request identifiers) and handle it
(return the error or set a safe fallback UnitCost) instead of discarding it;
update the call site where FromFeatureLLMUnitCost and resp.UnitCost are used to
handle the error path explicitly.
- Around line 200-234: resolveLLMPricing currently swallows errors from
svc.ResolvePrice; when ResolvePrice returns an error you should log the failure
(at debug level) with the error and contextual info (feat.Namespace, provider,
model) before returning nil. Locate resolveLLMPricing and the svc.ResolvePrice
call and add a debug log statement that extracts/uses the request context logger
(e.g. logger from ctx or the project's logging helper) to emit a message like
"failed to resolve LLM price" including err, feat.Namespace, provider and model,
then keep returning nil for not-found flows.

In `@api/v3/handlers/features/create.go`:
- Around line 35-46: When GetMeterByIDOrSlug (via
h.meterService.GetMeterByIDOrSlug) returns an error, wrap that error with
contextual text before returning so callers can see "failed to resolve meter"
plus the original error; update the error return in the meter resolution block
(the branch that sets meterSlug) to wrap the err (e.g., using fmt.Errorf("failed
to resolve meter %s: %w", body.Meter.Id, err) or errors.Wrap) and return the
wrapped error instead of the raw err.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73799da5-a448-4da3-8a09-6f76a0431a92

📥 Commits

Reviewing files that changed from the base of the PR and between ec91df1 and 5852d88.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (7)
  • api/spec/packages/aip/src/features/feature.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/features/convert.go
  • api/v3/handlers/features/convert_test.go
  • api/v3/handlers/features/create.go
  • api/v3/handlers/features/handler.go
  • api/v3/server/server.go

… costs

AsFeatureLLMUnitCost() silently succeeds on manual union variants,
causing FromFeatureLLMUnitCost() to overwrite the manual cost data.
Add discriminator check before attempting the LLM cast.

Added test for the manual unit cost no-op case that exposed the bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: 1

♻️ Duplicate comments (1)
api/v3/api.gen.go (1)

2177-2180: ⚠️ Potential issue | 🟡 Minor

Broaden the unit_cost docs to include static LLM values.

The wording still reads like llm only works through meter properties, but FeatureLLMUnitCost already supports static provider, model, and token_type values. Please update the source OpenAPI description and regenerate so client docs and SDKs don’t miss that form.

As per coding guidelines "Never manually edit generated files marked with '// Code generated by X, DO NOT EDIT.' headers", update the source OpenAPI spec and regenerate.

Also applies to: 2309-2312

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

In `@api/v3/api.gen.go` around lines 2177 - 2180, Update the OpenAPI source
description for the UnitCost/unit_cost property (the schema backing
FeatureUnitCost and FeatureLLMUnitCost) to state that when using "llm" it
supports either dynamic lookup via meter group-by properties or static LLM
configuration by specifying provider, model, and token_type fields in
FeatureLLMUnitCost; then regenerate the API client code so the generated file
(containing UnitCost, FeatureUnitCost, FeatureLLMUnitCost) and downstream SDK
docs reflect the expanded wording instead of editing the generated api.gen.go
directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/api.gen.go`:
- Around line 2168-2169: The feature flow is carrying the meter slug into the
generated API (meter.id) because create handler normalizes to m.Key and convert
uses f.MeterSlug; fix by plumbing the actual meter ULID through the domain
objects: in the create path (api/v3/handlers/features/create.go) resolve the
meter (using GetMeterByIDOrSlug or existing lookup used today), set a
new/existing Feature.MeterID (ULID) field on the Feature domain object before
persisting, ensure the persistence/save logic stores that MeterID, and update
the convert routine (api/v3/handlers/features/convert.go) to populate
resp.Meter.Id from f.MeterID (not f.MeterSlug); this keeps generated api.gen.go
correct without editing generated files.

---

Duplicate comments:
In `@api/v3/api.gen.go`:
- Around line 2177-2180: Update the OpenAPI source description for the
UnitCost/unit_cost property (the schema backing FeatureUnitCost and
FeatureLLMUnitCost) to state that when using "llm" it supports either dynamic
lookup via meter group-by properties or static LLM configuration by specifying
provider, model, and token_type fields in FeatureLLMUnitCost; then regenerate
the API client code so the generated file (containing UnitCost, FeatureUnitCost,
FeatureLLMUnitCost) and downstream SDK docs reflect the expanded wording instead
of editing the generated api.gen.go directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c649c985-e4f0-46ac-8ba6-fdad08dd6917

📥 Commits

Reviewing files that changed from the base of the PR and between 5852d88 and f49d57c.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (2)
  • api/spec/packages/aip/src/features/unitcost.tsp
  • api/v3/api.gen.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/spec/packages/aip/src/features/unitcost.tsp

Comment on lines +2168 to +2169
// Id The ID of the meter to associate with this feature.
Id ULID `json:"id"`
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

meter.id still looks wired to the meter slug.

This contract is ID-based now, but api/v3/handlers/features/create.go (Lines 35-46) still normalizes the request to m.Key, and api/v3/handlers/features/convert.go (Lines 18-42) writes f.MeterSlug back into resp.Meter.Id. Unless the domain model now carries the real meter ID too, responses will end up serializing a slug/key under meter.id. I’d carry or preload the actual meter ID through the feature flow instead of fixing this with per-feature lookups later.

Quick read-only check to confirm the drift. Expected result: the create path stores m.Key, and the convert path later feeds that slug/key into resp.Meter.Id.

#!/bin/bash
set -euo pipefail

printf '\n== api/v3/handlers/features/create.go ==\n'
sed -n '1,120p' api/v3/handlers/features/create.go

printf '\n== api/v3/handlers/features/convert.go ==\n'
sed -n '1,120p' api/v3/handlers/features/convert.go

printf '\n== feature meter fields/usages outside generated code ==\n'
rg -n -C3 --type go -g '!api/v3/api.gen.go' '\btype Feature struct\b|\bMeterSlug\b|\bMeterID\b|\bGetMeterByIDOrSlug\b'

As per coding guidelines "Never manually edit generated files marked with '// Code generated by X, DO NOT EDIT.' headers", fix this in the source spec and/or non-generated handlers rather than patching api.gen.go directly.

Also applies to: 2300-2301

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

In `@api/v3/api.gen.go` around lines 2168 - 2169, The feature flow is carrying the
meter slug into the generated API (meter.id) because create handler normalizes
to m.Key and convert uses f.MeterSlug; fix by plumbing the actual meter ULID
through the domain objects: in the create path
(api/v3/handlers/features/create.go) resolve the meter (using GetMeterByIDOrSlug
or existing lookup used today), set a new/existing Feature.MeterID (ULID) field
on the Feature domain object before persisting, ensure the persistence/save
logic stores that MeterID, and update the convert routine
(api/v3/handlers/features/convert.go) to populate resp.Meter.Id from f.MeterID
(not f.MeterSlug); this keeps generated api.gen.go correct without editing
generated files.

Validate filter dimension keys against the meter's GroupBy and reject
unknown filter operators at the API layer before reaching the domain.
Fix remaining unnecessary type conversions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hekike hekike marked this pull request as draft March 19, 2026 22:24
@hekike
Copy link
Contributor Author

hekike commented Mar 21, 2026

Closing in favor of: #3986

@hekike hekike closed this Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants