Skip to content

feat(pricing): add admin-managed model pricing overrides#306

Merged
SantiagoDePolonia merged 7 commits intomainfrom
feat/pricing-override-ui
May 7, 2026
Merged

feat(pricing): add admin-managed model pricing overrides#306
SantiagoDePolonia merged 7 commits intomainfrom
feat/pricing-override-ui

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented May 7, 2026

Summary

  • add admin-managed model pricing override storage/API/UI with exact, model-wide, provider-wide, and global scope priority
  • apply DB pricing overrides to usage cost calculation and show effective pricing previews in the dashboard
  • expose per-field pricing sources, update Swagger/OpenAPI, and preserve slash-shaped model IDs such as OpenRouter models

Tests

  • go test ./...
  • node --test internal/admin/dashboard/static/js/modules/*.test.cjs
  • git diff --check
  • pre-commit hook: make test-race, make lint, go mod tidy

Summary by CodeRabbit

  • New Features

    • Admin API + dashboard UI to list/create/update/delete selector-scoped model pricing overrides with modal editor, live pricing preview, tiered pricing, and pricing action buttons.
  • Behavior Changes

    • Pricing resolution now prefers the requested model/provider, records per-field pricing sources, and supports per-tier token caps.
  • Documentation

    • OpenAPI spec and admin docs updated with new pricing-override endpoints and schemas.
  • Tests

    • Expanded coverage for lifecycle, validation, storage backends, resolution precedence, and UI rendering.

SantiagoDePolonia and others added 2 commits May 7, 2026 18:39
Adds dashboard + API for operator-supplied USD pricing that shadows
model registry and config.yaml pricing in usage cost calculations. The
most-specific selector wins (provider+model > model > provider > global).

Also fixes pricing lookup in usage paths to use the canonical model id
(e.g. gpt-4o-mini) instead of the provider's versioned response model
(e.g. gpt-4o-mini-2024-07-18), which previously could miss base prices.

Selector grammar shared with model access overrides was extracted to a
new internal/modelselectors package. ModelMetadata now carries a
PricingSources map so the UI can show where each price came from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 7, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview May 7, 2026, 4:43 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

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
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a persisted USD model pricing overrides subsystem: selector normalization, stores (SQLite/Postgres/Mongo), an in-memory snapshot service with validation/rollback and background refresh, admin REST API and OpenAPI schemas, dashboard editor (UI/CSS/JS), app wiring for a pricingResolver, and tests.

Changes

Model Pricing Overrides System

Layer / File(s) Summary
Schemas / OpenAPI
cmd/gomodel/docs/docs.go, docs/openapi.json, tools/openapi-postprocess.mjs
Adds admin endpoints and schemas for model-overrides and model-pricing-overrides; enforces request/response bounds and enum constraints; extends core.ModelMetadata.pricing_sources and ModelPricingTier.up_to_tokens.
Selector Normalization
internal/modelselectors/selectors.go, internal/modeloverrides/types.go, internal/modeloverrides/store.go
New modelselectors package provides canonical parsing, normalization, scope classification, and validation errors; modeloverrides delegates selector logic to it.
Core Types & Pricing Sources
internal/core/types.go, internal/modeldata/merge.go, internal/modeldata/merger.go
Adds ModelMetadata.PricingSources, ModelPricing FieldSources/constants, and propagation/clone behavior for per-field pricing provenance when building/merging metadata.
Pricing Types & Validation
internal/pricingoverrides/types.go, internal/pricingoverrides/validation.go
Defines USD-only Pricing and PricingTier, persisted Override/View, normalization helpers, and validatePricing rules for scalars and tiered pricing.
Store Interface & Helpers
internal/pricingoverrides/store.go
Defines Store interface, ErrNotFound, ValidationError alias, and helpers to prepare overrides for persistence (JSON encoding, timestamps, normalization).
Store Implementations
internal/pricingoverrides/store_sqlite.go, .../store_postgresql.go, .../store_mongodb.go
Concrete SQLite/Postgres/Mongo stores with schema/index creation, JSON pricing payload handling, scanning/unmarshalling, and CRUD semantics.
Pricing Helpers
internal/pricingoverrides/pricing.go, internal/providers/registry_pricing_merge.go
Clone/convert/merge pricing into core.ModelPricing; provider registry merge functions updated to merge pricing and pricing-sources.
Snapshot & Service
internal/pricingoverrides/snapshot.go, internal/pricingoverrides/service.go, internal/pricingoverrides/resolver.go
Service maintains an atomic in-memory snapshot with precompiled lookup maps (exact/provider/model/global), supports List/Get/Upsert/Delete with prospective validation, rollback/reconciliation, background refresh, and ResolvePricing returning merged pricing.
Factory / Init / App wiring
internal/pricingoverrides/factory.go, internal/app/app.go
Factory constructs Result(Service, Store, Storage), runs initial Refresh, starts background refresh, and app initializes/wires pricingOverrides and a pricingResolver used across server/response cache/guardrails.
Admin Handlers / Routes / Errors
internal/admin/handler.go, internal/admin/handler_model_pricing_overrides.go, internal/admin/errors.go, internal/admin/routes.go
Adds handler options (WithPricingResolver, WithPricingOverrides), handlers for GET/PUT/DELETE pricing overrides, selector decoding with length cap, write-error mapping, route registrations, and tests.
Dashboard Templates, CSS & JS
internal/admin/dashboard/templates/*, internal/admin/dashboard/static/css/dashboard.css, internal/admin/dashboard/static/js/modules/model-pricing-overrides.js, internal/admin/dashboard/static/js/dashboard.js
Adds pricing override editor modal, model-global-action-buttons partial, dollar-icon template, unified .form-select styling, grid layouts for override rows and preview, responsive tweaks, and the JS module managing UI state, preview computation, validation, and API calls; dashboard integrates module loading and fetch orchestration.
Tests / QA
internal/pricingoverrides/*_test.go, internal/admin/handler_model_pricing_overrides_test.go, dashboard JS tests, gateway/usage/stream tests
Unit and integration tests for selector normalization, service precedence/validation/rollback, store persistence (SQLite), admin handler lifecycle/validation, dashboard module behavior, and requested-model pricing resolution tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–90 minutes

Possibly related PRs

Suggested labels

release:internal

Poem

🐰 A tiny coin in templates bright,

modal, tiers, and preview light.
Admins tap, the overrides stay—
saved in stores to rule the day.
Hoppity hop, the prices bite.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pricing-override-ui

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 7, 2026

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 7, 2026

Greptile Summary

Adds a complete admin-managed model pricing override subsystem: an in-memory-cached, store-backed service (SQLite/PostgreSQL/MongoDB) that resolves effective pricing at four scopes (exact → model-wide → provider-wide → global) and applies DB overrides on top of the base registry during usage cost calculation. The PR also threads a specific providerName (instead of the generic type) and the originally-requested model ID (instead of the versioned response model) through all pricing resolution call-sites.

  • New pricingoverrides package introduces Service with atomic snapshot swaps, rollback-with-reconciliation on refresh failure, and per-tier threshold validation (including explicit rejection of conflicting threshold units).
  • Gateway and stream-observer changes improve pricing accuracy by preferring the client-requested model ID over the potentially-versioned response model, and using the named provider instance for override lookup.
  • Admin CRUD endpoints (GET/PUT/DELETE /admin/api/v1/model-pricing-overrides/{selector}) expose the new feature with proper 400/404/503 error mapping and Swagger annotations.

Confidence Score: 5/5

The change is safe to merge; the new subsystem is well-isolated and the gateway modifications are narrowly scoped to improving pricing-model lookup accuracy.

The rollback-with-reconciliation path, atomic snapshot swaps, and tier validation are all handled correctly. All error paths in app.go include the new pricingOverrides.Close() call. The pricing resolution chain is correct across gateway, stream observer, and cache-hit paths.

internal/modelselectors/selectors.go — the trailing-slash input for an unregistered provider is silently folded into a model-only override rather than rejected.

Important Files Changed

Filename Overview
internal/pricingoverrides/service.go New service with atomic snapshot swap, rollback-with-reconciliation on refresh failure, and background refresh goroutine — all implemented correctly
internal/pricingoverrides/snapshot.go Builds and indexes overrides by exact/model/provider/global scope with duplicate-selector detection; priority order (exact > model-wide > provider-wide > global) matches PR spec
internal/pricingoverrides/validation.go Validates scalar non-negativity and tier constraints; now explicitly rejects tiers with both threshold fields set, fixing the previously flagged silent-discard issue
internal/modelselectors/selectors.go New selector normalization package; trailing-slash input for an unregistered provider is silently accepted as a model-only override with a slash-terminated model ID that never matches
internal/pricingoverrides/factory.go Wires storage, service, and background refresh; refresh interval is borrowed from cfg.Workflows.RefreshInterval without documentation, coupling two unrelated subsystems' cadences
internal/pricingoverrides/resolver.go Merges base registry pricing with the highest-priority matching DB override; strips provider prefix from slash-shaped model IDs before lookup
internal/gateway/usage.go Now passes providerName (instance name) over providerType (generic type) to ResolvePricing, enabling per-named-provider pricing overrides
internal/gateway/inference_execute.go Introduces usagePricingModel() to select the canonical pricing model ID (failover > workflow-resolved > response model) for more accurate override lookups
internal/usage/stream_observer.go pricingModel() and pricingProvider() helpers prefer the requested model/named provider over the versioned response model/type for pricing resolution
internal/app/app.go Adds pricingOverrides subsystem with shared-storage reuse, propagates pricingResolver through gateway and server, and correctly includes pricingOverrides.Close() in all error paths
internal/admin/handler_model_pricing_overrides.go CRUD handlers for pricing overrides with proper auth, validation error mapping to 400, and 503 guard when the feature is disabled
internal/providers/registry_pricing_merge.go New field-by-field config pricing merge and pricing-source tracking; replaces the previous full-replacement merge in applyConfigMetadataOverrides
internal/pricingoverrides/store_sqlite.go SQLite store with DDL-on-init, Unix-epoch timestamps, and JSON-serialized pricing column; prepareOverrideUpsert shared with PostgreSQL backend
internal/pricingoverrides/store_postgresql.go PostgreSQL store with JSONB pricing column and ON CONFLICT upsert; structure mirrors SQLite store, using shared prepareOverrideUpsert helper
internal/pricingoverrides/store_mongodb.go MongoDB store using native BSON struct marshalling and $set/$setOnInsert upsert pattern; CreatedAt preserved on update via $setOnInsert

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    REQ["Incoming Request\n(model, provider)"] --> GW["Gateway / StreamObserver\nusagePricingModel()"]
    GW --> |"pricingModel, providerName"| PR["pricingoverrides.Service\nResolvePricing()"]
    PR --> SNAP["Snapshot.matchingOverride()"]
    SNAP --> |"exact: provider+model"| E["exact[provider/model]"]
    SNAP --> |"model-wide"| M["modelWide[model]"]
    SNAP --> |"provider-wide"| P["providerWide[provider]"]
    SNAP --> |"global"| G["global override"]
    SNAP --> |"no match"| BASE["Base Registry Pricing"]
    E --> MERGE["mergePricing(base, override)"]
    M --> MERGE
    P --> MERGE
    G --> MERGE
    MERGE --> COST["Usage Cost Calculation"]
    BASE --> COST

    ADMIN["Admin API\nPUT /model-pricing-overrides/{selector}"] --> SVC["Service.Upsert()"]
    SVC --> NORM["NormalizeInput\n(catalog + selector)"]
    NORM --> STORE["Store.Upsert\n(SQLite/PG/Mongo)"]
    STORE --> REFRESH["refreshLocked()\natomic snapshot swap"]
    REFRESH --> SNAP
Loading

Reviews (3): Last reviewed commit: "fix(pricing): align tier schema validati..." | Re-trigger Greptile

Comment thread internal/pricingoverrides/resolver.go
Comment thread internal/gateway/inference_execute.go
Comment thread internal/modeloverrides/types.go Outdated
Comment thread internal/pricingoverrides/service.go
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 7, 2026

Greptile Summary

This PR introduces an admin-managed model pricing override system with four selector scopes (exact provider+model, model-wide, provider-wide, global), backed by SQLite/PostgreSQL/MongoDB stores, an atomic in-memory snapshot, and a usage cost resolver that applies DB overrides at inference time for both streaming and non-streaming paths.

  • New pricingoverrides package — store, service, snapshot, resolver, validation, and factory wired into App lifecycle; background refresh every 60 s with rollback-on-failure write semantics.
  • Usage pricing fixusagePricingModel now resolves the requested/workflow model (e.g., gpt-4o-mini) rather than the versioned response model (e.g., gpt-4o-mini-2024-07-18) when looking up DB pricing, fixing matching for OpenRouter slash-shaped IDs too.
  • Shared selector logicmodelselectors package extracted from modeloverrides, eliminating duplication; modeloverrides types now delegate to it.

Confidence Score: 3/5

The change is mostly safe to merge; the pricing resolution and snapshot logic are well-tested, but the nil-interface default in handler.go can produce a panic in the usage recalculation endpoint if wiring is ever incomplete.

The admin usage recalculation endpoint now calls h.pricingResolver which defaults to registry — a nil registry produces a non-nil interface wrapping a nil pointer. Any call to RecalculateUsagePricing without WithPricingResolver would panic rather than return a graceful error. This is a present risk on the changed path.

internal/admin/handler.go (nil-interface default for pricingResolver) and internal/pricingoverrides/validation.go (mutual-exclusivity of UpToTokens/UpToMtok per tier)

Important Files Changed

Filename Overview
internal/pricingoverrides/service.go Core pricing override service with in-memory atomic snapshot, background refresh, and rollback-on-failure upsert/delete — logic is sound but the stale-snapshot window during rollback failure is undocumented.
internal/pricingoverrides/snapshot.go Priority-ordered (exact > model-wide > provider-wide > global) snapshot lookup; well-tested and correct.
internal/pricingoverrides/validation.go Validates scalar and tiered pricing fields; missing mutual-exclusivity check between UpToTokens and UpToMtok on the same tier silently drops one value.
internal/pricingoverrides/factory.go Factory initializes the store, service, and background refresh; Close() is idempotent via sync.Once and Result is never nil on success.
internal/pricingoverrides/store_sqlite.go SQLite store with idiomatic upsert on conflict, Unix-epoch timestamps, and JSON-serialized pricing blob; no issues found.
internal/pricingoverrides/store_postgresql.go PostgreSQL store using JSONB and ON CONFLICT upsert; structure mirrors SQLite store cleanly.
internal/pricingoverrides/store_mongodb.go MongoDB store using $set/$setOnInsert upsert; created_at is correctly preserved on update via setOnInsert.
internal/admin/handler.go Added pricingResolver and pricingOverrides fields; the default pricingResolver: registry initialization creates a non-nil interface containing a nil pointer when registry is nil.
internal/admin/handler_model_pricing_overrides.go CRUD handlers for pricing overrides with proper nil-service guards and validation-to-400 error mapping; lifecycle test covers upsert, list, and delete.
internal/gateway/inference_execute.go usagePricingModel correctly prefers the resolved model over the versioned response model for pricing lookups; covered by new orchestrator test.
internal/modelselectors/selectors.go New shared selector normalization package extracted from modeloverrides; ParseStoredParts cannot distinguish slash-shaped model IDs without a catalog when DB columns are empty.
internal/app/app.go Pricing override subsystem wired into app lifecycle correctly; all error cleanup paths updated to include pricingOverrides.Close().
internal/usage/stream_observer.go pricingModel/pricingProvider helpers ensure stream usage pricing uses the requested model and specific provider name.
internal/core/types.go Added PricingSources map and FieldSources helper; Clone updated to deep-copy PricingSources; yaml:"-" tag correctly omits it from config serialization.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Gateway as InferenceOrchestrator
    participant PricingSvc as PricingOverrides.Service
    participant Snapshot as In-Memory Snapshot
    participant Store as DB Store
    participant UsageLog as UsageLogger

    Client->>Gateway: POST /v1/chat/completions
    Gateway->>Gateway: usagePricingModel resolves gpt-4o-mini
    Gateway->>PricingSvc: ResolvePricing(gpt-4o-mini, openai)
    PricingSvc->>Snapshot: matchingOverride(openai, gpt-4o-mini)
    Note right of Snapshot: Priority: exact > model-wide > provider-wide > global
    Snapshot-->>PricingSvc: override or nil
    PricingSvc-->>Gateway: merged ModelPricing
    Gateway->>UsageLog: Write entry with effective pricing
    Note over Store, Snapshot: Background refresh every 60s
    Store-->>Snapshot: store.List atomic snapshot swap
    participant AdminAPI as Admin API
    AdminAPI->>PricingSvc: Upsert Override
    PricingSvc->>Store: store.Upsert normalized
    PricingSvc->>Store: store.List refreshLocked
    Store-->>Snapshot: new snapshot stored atomically
    PricingSvc-->>AdminAPI: View with ScopeKind
Loading

Reviews (2): Last reviewed commit: "fix(pricing): preserve slash-shaped over..." | Re-trigger Greptile

Comment thread internal/pricingoverrides/validation.go
Comment thread internal/pricingoverrides/validation.go
Comment thread internal/pricingoverrides/service.go
Comment thread internal/admin/handler.go
Comment thread internal/modelselectors/selectors.go
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: 12

Caution

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

⚠️ Outside diff range comments (2)
internal/admin/handler_usage.go (1)

237-242: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

h.registry nil-guard doesn't protect the actual argument used in the call

Line 260 passes h.pricingResolver, but the nil guard on Line 241 checks h.registry. If wiring ever produces a non-nil h.registry alongside a nil h.pricingResolver (e.g., a custom pricing resolver path omits the default), the guard passes and line 260 panics. The guard should protect what it actually uses.

🛡️ Proposed fix
-	if h.registry == nil {
-		return handleError(c, featureUnavailableError("model pricing metadata is unavailable"))
-	}
+	if h.pricingResolver == nil {
+		return handleError(c, featureUnavailableError("model pricing metadata is unavailable"))
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/handler_usage.go` around lines 237 - 242, The nil-check
currently validates h.registry but the subsequent call uses h.pricingResolver,
so replace or add the guard to check h.pricingResolver (and keep the existing
h.registry check if both are required) before proceeding; specifically, ensure
that before the call that references h.pricingResolver you validate
h.pricingResolver != nil and return handleError(c,
featureUnavailableError("model pricing metadata is unavailable" or a clearer
message for missing pricing resolver)) if it's nil so the code cannot panic when
h.pricingResolver is absent.
internal/providers/registry_metadata.go (1)

321-329: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PricingSources is fully replaced instead of merged, silently dropping registry sources for non-overridden fields

When override.Pricing is set but only contains some fields (e.g., only InputPerMtok), calling FieldSources(...) returns entries only for those non-nil fields. Assigning that directly to merged.PricingSources replaces the entire map, discarding any registry-sourced entries that were already present for fields the override doesn't touch.

Example: base model from registry has {input_per_mtok: ModelRegistry, output_per_mtok: ModelRegistry}. A config YAML override sets only InputPerMtok. After line 323, PricingSources becomes {input_per_mtok: ConfigYAML} and output_per_mtok's source is lost—despite the merged price value still coming from the registry.

Merge into the existing map instead of replacing it:

Proposed fix
 if override.Pricing != nil {
-    merged.PricingSources = override.Pricing.FieldSources(core.ModelPricingSourceConfigYAML)
+    for field, src := range override.Pricing.FieldSources(core.ModelPricingSourceConfigYAML) {
+        if merged.PricingSources == nil {
+            merged.PricingSources = make(map[string]core.ModelPricingSource)
+        }
+        merged.PricingSources[field] = src
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/registry_metadata.go` around lines 321 - 329, The current
code replaces merged.PricingSources outright when override.Pricing is non-nil,
which discards registry-origin entries for fields the override doesn't touch;
instead, initialize merged.PricingSources if nil and copy/merge the entries from
override.Pricing.FieldSources(core.ModelPricingSourceConfigYAML) into the
existing merged.PricingSources map (overwriting only the keys present in the
override), so registry sources for untouched fields are preserved; update the
block around merged := modeldata.MergeMetadata(...) / if override.Pricing != nil
{ ... } to perform a map-merge rather than direct assignment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/gomodel/docs/docs.go`:
- Around line 5421-5438: The OpenAPI enum for pricingoverrides.ScopeKind
contains duplicated entries; update the "pricingoverrides.ScopeKind" enum array
to contain only the four unique values in the same order as x-enum-varnames
("global", "model", "provider", "provider_model") so the enum length matches
x-enum-varnames and client generation won't break.

In `@docs/openapi.json`:
- Around line 830-835: The array response schema for the modeloverrides list
endpoints is unbounded; update the array schema (the object with "type": "array"
and "items": {"$ref":"#/components/schemas/modeloverrides.View"}) to include a
reasonable "maxItems" property (and optionally "minItems") to cap the response
size so clients have clear bounds; apply the same change to both occurrences
(the one referencing modeloverrides.View at the shown location and the other
occurrence around lines 1034-1038).
- Around line 5362-5369: The schema admin.upsertModelPricingOverrideRequest
currently defines a pricing property but doesn't mark it required; update this
schema to include "required": ["pricing"] so the pricing field (referencing
components.schemas.pricingoverrides.Pricing) is documented as mandatory for the
request payload, ensuring the OpenAPI spec reflects the API requirement.
- Around line 7023-7034: The pricingoverrides.ScopeKind enum in the OpenAPI spec
contains duplicated literals; update the pricingoverrides.ScopeKind schema to
list each enum value only once (keep "global", "model", "provider",
"provider_model" as unique entries) so the enum array has no duplicates, then
save the modified openapi.json (and regenerate any generated clients/schemas if
applicable) to ensure validators and generators accept the cleaned enum.

In `@internal/admin/dashboard/templates/page-models.html`:
- Around line 228-239: The labels lack for attributes and the select/input lack
unique ids, so add bound ids using the row identifier and reference them from
the labels; for the select in the pricing-override-type-field create an id bound
to the row (e.g. "pricing-type-" + row.id) and set the label's for to the same
bound id, and likewise for the pricing-override-value-field create a bound id
(e.g. "pricing-value-" + row.id) and set that label's for to match so the
elements tied to x-model="row.field" and x-model="row.value" are properly
associated and unique per row.

In `@internal/admin/handler_model_pricing_overrides_test.go`:
- Around line 95-103: The test only checks HTTP status for
ListModelPricingOverrides; extend it to assert the response body contains the
expected items by decoding listRec.Body into the expected response struct or
slice and comparing key fields (e.g., model ID, price, or override entries)
against the expected fixtures set up in the test; update the test in
internal/admin/handler_model_pricing_overrides_test.go to unmarshal listRec.Body
(after calling h.ListModelPricingOverrides) and use assertions to validate the
returned list length and at least one representative record matches the expected
values for the created overrides.

In `@internal/admin/handler.go`:
- Around line 253-257: NewHandler assigns pricingResolver: registry which
creates a non-nil interface wrapping a nil concrete when registry is nil; change
NewHandler so it explicitly sets the pricingResolver interface to a true nil
when registry is nil (e.g., declare a local var pr PricingResolver = nil and if
registry != nil set pr = registry) and then assign pricingResolver: pr, ensuring
RecalculateUsagePricing can safely check the interface for nil; update
references to the Handler fields (pricingResolver, registry) as needed.

In `@internal/core/types.go`:
- Around line 262-301: Add brief godoc comments above the exported constants
ModelPricingSourceModelRegistry and ModelPricingSourceConfigYAML describing they
are string identifiers for where pricing data originated (registry vs config
YAML), and add a godoc comment for the exported method
(*ModelPricing).FieldSources explaining it returns a map of non-empty pricing
field names to the provided source string, that callers should pass a non-empty
source, and that pricing tiers (Tiers) are reported only as a single
coarse-grained "tiers" key rather than per-tier entries; place these comments
directly above the constants and the FieldSources method declarations.

In `@internal/modeldata/merger_test.go`:
- Around line 154-156: TestResolve_ProviderModelOverride currently only asserts
meta.PricingSources["input_per_mtok"]; add a parallel assertion that
meta.PricingSources["output_per_mtok"] equals
core.ModelPricingSourceModelRegistry so both InputPerMtok and OutputPerMtok from
the provider override are covered; update the test to check
meta.PricingSources["output_per_mtok"] with the same t.Errorf pattern used for
"input_per_mtok" to maintain consistency.

In `@internal/pricingoverrides/resolver.go`:
- Around line 10-23: Normalize providerName and model (call strings.TrimSpace on
providerName and modelIDFromSelector(model, providerName)) before invoking
base.ResolvePricing so the base lookup uses the same normalized identifiers;
move the providerName/model normalization above the base pricing lookup and pass
the normalized model and providerName into s.base.ResolvePricing (references:
ResolvePricing, modelIDFromSelector, providerName, base.ResolvePricing,
cloneBasePricing, mergePricing, snapshot().matchingOverride) while preserving
the nil checks and existing clone/merge behavior.

In `@internal/pricingoverrides/validation.go`:
- Around line 14-23: The loop validating p.Tiers must also reject tiers that set
both UpToTokens and UpToMtok to avoid silent precedence of UpToTokens; inside
the same validation logic that checks tier.UpToTokens/UpToMtok and the threshold
presence (the loop iterating over p.Tiers, e.g., in the Validate method where
you reference tier.UpToTokens and tier.UpToMtok), add a guard that if
tier.UpToTokens != nil && tier.UpToMtok != nil return
newValidationError("pricing.tiers only one of up_to_tokens/up_to_mtok may be
set", nil); apply the same check in the other validation block mentioned (the
one at the ~67-75 range) so both entry points reject dual-specified limits.

---

Outside diff comments:
In `@internal/admin/handler_usage.go`:
- Around line 237-242: The nil-check currently validates h.registry but the
subsequent call uses h.pricingResolver, so replace or add the guard to check
h.pricingResolver (and keep the existing h.registry check if both are required)
before proceeding; specifically, ensure that before the call that references
h.pricingResolver you validate h.pricingResolver != nil and return
handleError(c, featureUnavailableError("model pricing metadata is unavailable"
or a clearer message for missing pricing resolver)) if it's nil so the code
cannot panic when h.pricingResolver is absent.

In `@internal/providers/registry_metadata.go`:
- Around line 321-329: The current code replaces merged.PricingSources outright
when override.Pricing is non-nil, which discards registry-origin entries for
fields the override doesn't touch; instead, initialize merged.PricingSources if
nil and copy/merge the entries from
override.Pricing.FieldSources(core.ModelPricingSourceConfigYAML) into the
existing merged.PricingSources map (overwriting only the keys present in the
override), so registry sources for untouched fields are preserved; update the
block around merged := modeldata.MergeMetadata(...) / if override.Pricing != nil
{ ... } to perform a map-merge rather than direct assignment.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec057796-5fff-41b6-b920-cd38533afb7f

📥 Commits

Reviewing files that changed from the base of the PR and between fbb05fc and 92087b5.

📒 Files selected for processing (53)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs
  • internal/admin/dashboard/static/js/modules/model-pricing-overrides.js
  • internal/admin/dashboard/static/js/modules/model-pricing-overrides.test.cjs
  • internal/admin/dashboard/templates/dollar-icon.html
  • internal/admin/dashboard/templates/layout.html
  • internal/admin/dashboard/templates/model-table-body.html
  • internal/admin/dashboard/templates/page-budgets.html
  • internal/admin/dashboard/templates/page-guardrails.html
  • internal/admin/dashboard/templates/page-models.html
  • internal/admin/dashboard/templates/page-settings.html
  • internal/admin/dashboard/templates/page-workflows.html
  • internal/admin/errors.go
  • internal/admin/handler.go
  • internal/admin/handler_model_overrides.go
  • internal/admin/handler_model_pricing_overrides.go
  • internal/admin/handler_model_pricing_overrides_test.go
  • internal/admin/handler_usage.go
  • internal/admin/routes.go
  • internal/admin/routes_test.go
  • internal/app/app.go
  • internal/core/types.go
  • internal/gateway/inference_execute.go
  • internal/gateway/inference_orchestrator_test.go
  • internal/gateway/usage.go
  • internal/modeldata/merge.go
  • internal/modeldata/merger.go
  • internal/modeldata/merger_test.go
  • internal/modeloverrides/service.go
  • internal/modeloverrides/store.go
  • internal/modeloverrides/types.go
  • internal/modelselectors/selectors.go
  • internal/pricingoverrides/factory.go
  • internal/pricingoverrides/pricing.go
  • internal/pricingoverrides/resolver.go
  • internal/pricingoverrides/service.go
  • internal/pricingoverrides/service_test.go
  • internal/pricingoverrides/snapshot.go
  • internal/pricingoverrides/store.go
  • internal/pricingoverrides/store_mongodb.go
  • internal/pricingoverrides/store_postgresql.go
  • internal/pricingoverrides/store_sqlite.go
  • internal/pricingoverrides/store_sqlite_test.go
  • internal/pricingoverrides/types.go
  • internal/pricingoverrides/validation.go
  • internal/providers/registry_metadata.go
  • internal/providers/registry_metadata_override_test.go
  • internal/responsecache/usage_hit.go
  • internal/usage/stream_observer.go
  • internal/usage/stream_observer_test.go

Comment thread cmd/gomodel/docs/docs.go
Comment thread docs/openapi.json
Comment thread docs/openapi.json
Comment thread docs/openapi.json
Comment thread internal/admin/dashboard/templates/page-models.html
Comment thread internal/app/app.go
Comment on lines +230 to +248
var pricingOverrideResult *pricingoverrides.Result
sharedPricingOverrideStorage := firstSharedStorage(auditResult.Storage, usageResult.Storage, batchResult.Storage, aliasResult.Storage, modelOverrideResult.Storage)
if sharedPricingOverrideStorage != nil {
pricingOverrideResult, err = pricingoverrides.NewWithSharedStorage(ctx, appCfg, sharedPricingOverrideStorage, providerResult.Registry, providerResult.Registry)
} else {
pricingOverrideResult, err = pricingoverrides.New(ctx, appCfg, providerResult.Registry, providerResult.Registry)
}
if err != nil {
closeErr := errors.Join(app.modelOverrides.Close(), app.aliases.Close(), app.batch.Close(), app.budgets.Close(), app.usage.Close(), app.audit.Close(), app.providers.Close())
if closeErr != nil {
return nil, fmt.Errorf("failed to initialize model pricing overrides: %w (also: close error: %v)", err, closeErr)
}
return nil, fmt.Errorf("failed to initialize model pricing overrides: %w", err)
}
app.pricingOverrides = pricingOverrideResult
pricingResolver := usage.PricingResolver(providerResult.Registry)
if app.pricingOverrides != nil && app.pricingOverrides.Service != nil {
pricingResolver = app.pricingOverrides.Service
}
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the pricingoverrides package constructors to confirm the two trailing
# arguments are intentionally the same registry, and check for any feature flag.
fd -t f 'pricingoverrides' -e go | head -50
echo "---- New / NewWithSharedStorage signatures ----"
rg -nP -C2 '\bfunc\s+(New|NewWithSharedStorage)\s*\(' --type=go -g 'internal/pricingoverrides/**'
echo "---- config flags referencing pricing overrides ----"
rg -nP -C2 'PricingOverride' --type=go -g 'config/**'

Repository: ENTERPILOT/GoModel

Length of output: 1152


🏁 Script executed:

# Check what types Catalog and usage.PricingResolver are, and what providerResult.Registry is
rg -nP 'type\s+(Catalog|PricingResolver)' --type=go
echo "---- providerResult.Registry type context ----"
rg -nP 'providerResult\.Registry' --type=go -B2 -A2 | head -60

Repository: ENTERPILOT/GoModel

Length of output: 3694


🏁 Script executed:

# Search for feature flags or config related to pricing overrides with broader patterns
rg -nP 'PricingOverride|pricing.*[Ee]nable|pricing.*[Cc]onfig' --type=go internal/config/ | head -40

Repository: ENTERPILOT/GoModel

Length of output: 119


🏁 Script executed:

# Check the shutdown/close comments around the lines mentioned in the scratchpad
sed -n '700,770p' internal/app/app.go | cat -n

Repository: ENTERPILOT/GoModel

Length of output: 2649


🏁 Script executed:

# Check the Catalog interface definition from modelselectors
rg -nP 'type Catalog interface' --type=go -A10 internal/modelselectors/

Repository: ENTERPILOT/GoModel

Length of output: 701


🏁 Script executed:

# Find Registry type definition and see what it implements
rg -nP 'type.*Registry|func.*NewRegistry|providerResult.*Registry\s*=' --type=go -B2 -A5 | head -100

Repository: ENTERPILOT/GoModel

Length of output: 3978


🏁 Script executed:

# Check if Registry has methods that satisfy Catalog interface
rg -nP 'func.*\(.*\*Registry\)|func.*\(.*Registry\)' --type=go | grep -i 'catalog\|model\|select' | head -20

Repository: ENTERPILOT/GoModel

Length of output: 2217


🏁 Script executed:

# Check if ModelRegistry has ProviderNames method
rg -nP 'ProviderNames' --type=go

Repository: ENTERPILOT/GoModel

Length of output: 2654


🏁 Script executed:

# Check the PricingResolver interface definition
rg -nP 'type PricingResolver interface' --type=go -A10

Repository: ENTERPILOT/GoModel

Length of output: 245


🏁 Script executed:

# Look for any config structures related to pricing to confirm there's no feature flag
fd -t f -e go -path '*/config/*' | head -20

Repository: ENTERPILOT/GoModel

Length of output: 231


🏁 Script executed:

# Check if ModelRegistry implements ResolvePricing method
rg -nP 'ResolvePricing|func.*\(.*\*ModelRegistry\).*ResolvePricing' --type=go

Repository: ENTERPILOT/GoModel

Length of output: 4943


🏁 Script executed:

# Check the pricingoverrides initialization again - verify if there's any conditional logic
sed -n '228,250p' internal/app/app.go | cat -n

Repository: ENTERPILOT/GoModel

Length of output: 1528


🏁 Script executed:

# Check if there are any admin handlers or feature checks that gate pricing overrides access
rg -nP 'pricingOverrides|pricing.*[Oo]verride' --type=go internal/admin/ -B2 -A2

Repository: ENTERPILOT/GoModel

Length of output: 19436


Pricing overrides initialization is unconditional — confirm this matches your design intent.

Unlike modelOverrideResult, which is gated by appCfg.Models.OverridesEnabled (line 210) and stubbed via &modeloverrides.Result{} otherwise, pricingoverrides.New[WithSharedStorage] runs unconditionally. Since admin handlers check if h.pricingOverrides == nil to return featureUnavailableError, and the initialization never produces a nil result, this nil check will never trigger. If pricing overrides should be a toggleable feature, add a config flag to conditionally initialize it like model overrides do.

Additionally, fix duplicate step numbering in shutdown sequence: lines 742–750 both label their comments as "10." Should be 10 and 11 respectively.

Comment thread internal/core/types.go
Comment thread internal/modeldata/merger_test.go
Comment thread internal/pricingoverrides/resolver.go
Comment thread internal/pricingoverrides/validation.go
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: 3

♻️ Duplicate comments (1)
internal/pricingoverrides/resolver.go (1)

14-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize model/providerName before invoking base.ResolvePricing.

s.base.ResolvePricing(model, providerName) at line 16 receives the raw selector form. When model arrives as "openai/gpt-4o", the base resolver (which keys by canonical model ID) will miss, and a partial DB override below ends up merged onto a nil base — silently dropping base pricing fields. Move the trim/modelIDFromSelector normalization above the base lookup so both paths see consistent identifiers.

🛠 Proposed fix
 func (s *Service) ResolvePricing(model, providerName string) *core.ModelPricing {
 	if s == nil {
 		return nil
 	}
+	providerName = strings.TrimSpace(providerName)
+	model = modelIDFromSelector(model, providerName)
+
 	var basePricing *core.ModelPricing
 	if s.base != nil {
 		basePricing = s.base.ResolvePricing(model, providerName)
 	}
-
-	providerName = strings.TrimSpace(providerName)
-	model = modelIDFromSelector(model, providerName)
 	if rule, ok := s.snapshot().matchingOverride(providerName, model); ok {
 		return mergePricing(basePricing, rule.override.Pricing)
 	}
 	return cloneBasePricing(basePricing)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pricingoverrides/resolver.go` around lines 14 - 23, The base resolve
is called with raw selector values causing mismatches; normalize providerName
and model first by trimming providerName and calling modelIDFromSelector(model,
providerName) before invoking s.base.ResolvePricing so
s.base.ResolvePricing(model, providerName) and the later matchingOverride lookup
use the same canonical identifiers; update the code flow in resolver.go to
perform providerName = strings.TrimSpace(providerName) and model =
modelIDFromSelector(model, providerName) prior to calling s.base.ResolvePricing,
keeping the subsequent snapshot().matchingOverride and mergePricing logic
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/gateway/inference_execute.go`:
- Around line 352-361: The function usagePricingModel should defensively trim
requestedModel before using it; modify usagePricingModel to call
strings.TrimSpace on requestedModel at the top (similar to
failoverModel/responseModel) and use the trimmed value when calling
ResolvedModelFromWorkflow and returning—this ensures callers that don't go
through requestModel won't pass untrimmed values into ResolvedModelFromWorkflow.

In `@internal/modelselectors/selectors.go`:
- Around line 119-132: The cached boolean globalSelector (computed via
IsGlobal(selector) at the top) should be reused instead of calling
IsGlobal(selector) again later; replace the second IsGlobal(selector) check with
the existing globalSelector variable so the checks are consistent and avoid
redundant trimming/compare—update the conditional that currently reads "if
providerName == "" && model == "" && !IsGlobal(selector) { return Selector{},
NewValidationError(...)" to use globalSelector, keeping the surrounding logic
that may call String(providerName, model) and ParseStoredParts unchanged and
preserving the same error return (Selector{}, NewValidationError).

In `@internal/pricingoverrides/service.go`:
- Around line 196-230: StartBackgroundRefresh can be given an interval shorter
than the per-tick refresh timeout (30s) which can cause ticker ticks to pile up
and drift; update StartBackgroundRefresh to enforce or clamp the interval to a
sensible minimum (e.g. at least the refresh timeout) instead of silently
accepting smaller values: introduce a named constant for the per-refresh timeout
(e.g. refreshTimeout := 30*time.Second), ensure the passed interval is set to
max(interval, refreshTimeout) (or refreshTimeout + small buffer) before creating
the ticker, and update the function comment to document the required minimum
interval and that the per-refresh timeout is refreshTimeout; reference
StartBackgroundRefresh, the refresh timeout constant, and s.Refresh in your
change.

---

Duplicate comments:
In `@internal/pricingoverrides/resolver.go`:
- Around line 14-23: The base resolve is called with raw selector values causing
mismatches; normalize providerName and model first by trimming providerName and
calling modelIDFromSelector(model, providerName) before invoking
s.base.ResolvePricing so s.base.ResolvePricing(model, providerName) and the
later matchingOverride lookup use the same canonical identifiers; update the
code flow in resolver.go to perform providerName =
strings.TrimSpace(providerName) and model = modelIDFromSelector(model,
providerName) prior to calling s.base.ResolvePricing, keeping the subsequent
snapshot().matchingOverride and mergePricing logic 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 599bc572-6385-4c44-b5c3-d8d76ee2c316

📥 Commits

Reviewing files that changed from the base of the PR and between 92087b5 and 0565414.

📒 Files selected for processing (9)
  • internal/admin/handler.go
  • internal/admin/handler_pricing_test.go
  • internal/gateway/inference_execute.go
  • internal/modeloverrides/types.go
  • internal/modelselectors/selectors.go
  • internal/pricingoverrides/resolver.go
  • internal/pricingoverrides/service.go
  • internal/pricingoverrides/service_test.go
  • internal/pricingoverrides/validation.go

Comment thread internal/gateway/inference_execute.go
Comment thread internal/modelselectors/selectors.go
Comment thread internal/pricingoverrides/service.go
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: 6

Caution

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

⚠️ Outside diff range comments (1)
internal/modeloverrides/service.go (1)

136-145: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add selector-match regression tests for the shared exact-key path.

Both snapshot population and lookup now depend on modelselectors.ExactMatchKey(). Please add a table-driven test that exercises exact, model-wide, provider-wide, and global matches, plus a raw slash-containing model ID whose prefix is not a configured provider. That will catch any silent drift between normalization and key generation before exact overrides become unresolvable.

As per coding guidelines, "Add or update tests for behavior changes."

Also applies to: 423-425

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/modeloverrides/service.go` around lines 136 - 145, Add a
table-driven unit test that verifies selector matching for all scopes using the
same ExactMatchKey path: construct test cases for exact (provider+model),
model-wide, provider-wide, global (matching modelselectors.ScopeProviderModel,
ScopeProvider, ScopeGlobal, default model-wide), and a raw model ID containing a
slash whose prefix is not a configured provider; for each case assert that the
snapshot population uses modelselectors.ExactMatchKey(normalized.ProviderName,
normalized.Model) to populate next.exact and that lookup resolves the same key
(check next.exact, next.modelWide, next.providerWide, next.global) so any drift
between normalization and ExactMatchKey is detected.
♻️ Duplicate comments (1)
internal/providers/registry_metadata_override_test.go (1)

74-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert output_per_mtok provenance in this init-path test as well.

This fixture overrides both InputPerMtok and OutputPerMtok, but the test only verifies the input source. Add the parallel output_per_mtok == core.ModelPricingSourceConfigYAML check so a one-sided provenance regression in Initialize fails here too.

Proposed addition
 if got := overridden.Model.Metadata.PricingSources["input_per_mtok"]; got != core.ModelPricingSourceConfigYAML {
 	t.Errorf("PricingSources[input_per_mtok] = %q, want %q", got, core.ModelPricingSourceConfigYAML)
 }
+if got := overridden.Model.Metadata.PricingSources["output_per_mtok"]; got != core.ModelPricingSourceConfigYAML {
+	t.Errorf("PricingSources[output_per_mtok] = %q, want %q", got, core.ModelPricingSourceConfigYAML)
+}

As per coding guidelines, "Add or update tests for behavior changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/registry_metadata_override_test.go` around lines 74 - 76,
The test only asserts overridden.Model.Metadata.PricingSources["input_per_mtok"]
equals core.ModelPricingSourceConfigYAML but misses the parallel check for
"output_per_mtok"; update the test in registry_metadata_override_test.go to also
assert that overridden.Model.Metadata.PricingSources["output_per_mtok"] ==
core.ModelPricingSourceConfigYAML so that the Initialize-path override for
OutputPerMtok is verified (use the same t.Errorf pattern as the existing
input_per_mtok assertion and reference the same overridden variable and
core.ModelPricingSourceConfigYAML constant).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/openapi.json`:
- Around line 818-868: Update the GET operation description for the
"/admin/api/v1/model-overrides" endpoint to explicitly document the four
selector formats (global as "/", provider as "provider/", model as "model", and
exact as "provider/model") mirroring the phrasing used in GET
/admin/api/v1/model-pricing-overrides; edit the "description" value under the
GET for "/admin/api/v1/model-overrides" so it lists those selector examples and
matches the parameter wording used by the PUT counterparts to ensure
consistency.
- Around line 5355-5365: The admin.upsertModelOverrideRequest schema's
user_paths array is unbounded; add a sensible maxItems (e.g., 100) to the
user_paths property and optionally add a maxLength (e.g., 1024) to the string
items to match bounding used elsewhere; update the
"admin.upsertModelOverrideRequest" schema's "user_paths" definition to include
"maxItems" and add "maxLength" under the items object so clients and linters
have an explicit cap.
- Around line 6918-6923: The duplicate x-enum-varnames for
modeloverrides.ScopeKind and pricingoverrides.ScopeKind can collide in some
generators; either (A) change the x-enum-varnames in each schema to unique,
schema-prefixed identifiers (e.g., ModelScopeGlobal/ModelScopeModel/... for
modeloverrides.ScopeKind and PricingScopeGlobal/... for
pricingoverrides.ScopeKind) by updating the x-enum-varnames arrays, or (B)
refactor both schema definitions to reference a single shared ScopeKind
component (remove duplicate local definitions and point modeloverrides.ScopeKind
and pricingoverrides.ScopeKind to a shared components/schemas/ScopeKind) so only
one x-enum-varnames set exists.
- Around line 897-906: Update the OpenAPI response for PUT
/admin/api/v1/model-overrides/{selector} to return modeloverrides.View (not
modeloverrides.Override) so it includes scope_kind (matching PUT
/admin/api/v1/model-pricing-overrides which returns pricingoverrides.View); then
update the corresponding handler that serves PUT
/admin/api/v1/model-overrides/{selector} to construct and return a
modeloverrides.View object (with scope_kind computed/populated) instead of
returning the Override shape. Ensure the Swagger annotation/schema in
docs/openapi.json is changed from "$ref":
"#/components/schemas/modeloverrides.Override" to "$ref":
"#/components/schemas/modeloverrides.View" and the handler that handles that
route returns the View type.

In `@internal/admin/handler_model_pricing_overrides_test.go`:
- Around line 64-129: TestModelPricingOverrideLifecycle only exercises a
selector without embedded slashes; add a second lifecycle sub-case that uses a
selector whose model ID contains slashes (e.g. selector
"openrouter/meta-llama/llama-3.1-8b-instruct") to exercise URL-decoding and
round-tripping. For the new case, create HTTP requests with the selector
URL-encoded in the path (e.g. openrouter%2Fmeta-llama%2Fllama-3.1-8b-instruct)
but set the Echo path value for "selector" to the raw slash-containing string,
then call UpsertModelPricingOverride, ListModelPricingOverrides and
DeleteModelPricingOverride and assert the returned View fields (Selector,
ProviderName, Model, ScopeKind, Pricing.InputPerMtok) match the expected parsed
parts to ensure the handler correctly preserves and parses slash-containing
model IDs.

In `@internal/pricingoverrides/snapshot.go`:
- Around line 39-60: The snapshot build loop currently appends every
normalized.Selector into next.order and blindly writes into next.bySelector and
the scope maps, so two stored overrides that normalize to the same selector will
silently overwrite one another; after calling normalizeStoredOverride (in the
loop that builds next.order/next.bySelector/compiledOverride) check if
normalized.Selector already exists in next.bySelector and reject by returning an
error (include both the normalized selector and the original override.Selector
in the message) rather than overwriting or appending a duplicate; this prevents
writing into the scope maps (next.global, next.exact, next.providerWide,
next.modelWide) for a colliding selector and avoids duplicate entries in
next.order and snapshotOverrides output.

---

Outside diff comments:
In `@internal/modeloverrides/service.go`:
- Around line 136-145: Add a table-driven unit test that verifies selector
matching for all scopes using the same ExactMatchKey path: construct test cases
for exact (provider+model), model-wide, provider-wide, global (matching
modelselectors.ScopeProviderModel, ScopeProvider, ScopeGlobal, default
model-wide), and a raw model ID containing a slash whose prefix is not a
configured provider; for each case assert that the snapshot population uses
modelselectors.ExactMatchKey(normalized.ProviderName, normalized.Model) to
populate next.exact and that lookup resolves the same key (check next.exact,
next.modelWide, next.providerWide, next.global) so any drift between
normalization and ExactMatchKey is detected.

---

Duplicate comments:
In `@internal/providers/registry_metadata_override_test.go`:
- Around line 74-76: The test only asserts
overridden.Model.Metadata.PricingSources["input_per_mtok"] equals
core.ModelPricingSourceConfigYAML but misses the parallel check for
"output_per_mtok"; update the test in registry_metadata_override_test.go to also
assert that overridden.Model.Metadata.PricingSources["output_per_mtok"] ==
core.ModelPricingSourceConfigYAML so that the Initialize-path override for
OutputPerMtok is verified (use the same t.Errorf pattern as the existing
input_per_mtok assertion and reference the same overridden variable and
core.ModelPricingSourceConfigYAML constant).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31f83de6-f2c3-46c5-b4e1-feb545c076e2

📥 Commits

Reviewing files that changed from the base of the PR and between 0565414 and 1b91d68.

📒 Files selected for processing (18)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs
  • internal/admin/dashboard/templates/page-models.html
  • internal/admin/handler_model_pricing_overrides.go
  • internal/admin/handler_model_pricing_overrides_test.go
  • internal/admin/handler_usage.go
  • internal/core/types.go
  • internal/modeldata/merger_test.go
  • internal/modeloverrides/service.go
  • internal/modeloverrides/types.go
  • internal/pricingoverrides/resolver.go
  • internal/pricingoverrides/snapshot.go
  • internal/pricingoverrides/types.go
  • internal/providers/registry_metadata.go
  • internal/providers/registry_metadata_override_test.go
  • internal/providers/registry_pricing_merge.go
  • tools/openapi-postprocess.mjs

Comment thread docs/openapi.json
Comment thread docs/openapi.json
Comment thread docs/openapi.json
Comment thread docs/openapi.json
Comment thread internal/admin/handler_model_pricing_overrides_test.go
Comment thread internal/pricingoverrides/snapshot.go
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: 10

Caution

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

⚠️ Outside diff range comments (1)
internal/gateway/inference_execute.go (1)

100-120: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ExecuteEmbeddings bypasses usagePricingModel, missing the requested-model pricing resolution.

Chat and responses now use usagePricingModel(workflow, requestedModel, ...) so that admin pricing overrides keyed on the canonical requested model are found even when the provider echoes a versioned model in the response. ExecuteEmbeddings still passes resp.Model directly to logUsage, so any pricing override on the canonical embedding model name would be silently skipped if the response model differs.

Since embeddings have no failover, the failoverModel branch of usagePricingModel would never fire — the only risk is the requested-vs-response model mismatch. A minimal fix aligns all three paths:

⚡ Proposed fix
 func (o *InferenceOrchestrator) ExecuteEmbeddings(ctx context.Context, workflow *core.Workflow, req *core.EmbeddingRequest, requestID, endpoint string) (*EmbeddingResult, error) {
 	if err := o.validateProviderAndRequest(req != nil, "embeddings request is required"); err != nil {
 		return nil, err
 	}
 	resp, providerType, providerName, err := o.executeEmbeddings(ctx, workflow, req)
 	if err != nil {
 		return nil, err
 	}
-	o.logUsage(ctx, workflow, resp.Model, providerType, providerName, func(pricing *core.ModelPricing) *usage.UsageEntry {
+	pricingModel := usagePricingModel(workflow, req.Model, "", resp.Model)
+	o.logUsage(ctx, workflow, pricingModel, providerType, providerName, func(pricing *core.ModelPricing) *usage.UsageEntry {
 		return usage.ExtractFromEmbeddingResponse(resp, requestID, providerType, endpoint, pricing)
 	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/gateway/inference_execute.go` around lines 100 - 120,
ExecuteEmbeddings currently passes resp.Model into logUsage which skips admin
pricing overrides keyed on the canonical requested model; instead call
usagePricingModel with the requested model and the response model to resolve the
correct pricing model (e.g., pricingModel := usagePricingModel(workflow,
req.Model, resp.Model)) and pass that resolved pricingModel into the logUsage
callback used by usage.ExtractFromEmbeddingResponse; keep EmbeddingResult.Model
as resp.Model but ensure logUsage uses the resolved pricing model.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/openapi.json`:
- Around line 6371-6372: The tier schemas currently declare up_to_tokens with
"type": "number", allowing fractional token counts; change the schema property
up_to_tokens to use "type": "integer" (and optionally add "minimum": 0) in both
schema definitions referenced (the up_to_tokens property occurrences in the tier
schemas), so token counts are validated as whole non-negative integers.
- Around line 6956-7014: The pricingoverrides.Pricing schema currently allows an
empty object which permits no-op upserts; update the schema
(pricingoverrides.Pricing) to disallow empty payloads by adding a JSON Schema
constraint such as "minProperties": 1 (or explicitly list required fields if
specific properties must always be present) so that
admin.upsertModelPricingOverrideRequest cannot pass an empty pricing object.

In `@internal/admin/handler_model_overrides.go`:
- Around line 51-54: The Swagger docs are missing the 502 failure annotation for
write operations; update the handler annotations for UpsertModelOverride and
DeleteModelOverride to include "@Failure 502 {object} core.GatewayError" so the
OpenAPI contract reflects provider/store write errors (also add the same
annotation near the other similar block around the second annotation group
referenced). Locate the comment blocks above the UpsertModelOverride and
DeleteModelOverride handlers in internal/admin/handler_model_overrides.go and
insert the 502 failure line in each relevant `@Failure` list.

In `@internal/admin/handler_model_pricing_overrides_test.go`:
- Around line 116-120: The test bypasses URL-decoding by injecting the
already-decoded selector via putCtx.SetPathValues; change the setup so the
handler actually decodes tt.encodedPath: remove the SetPathValues call and
either (A) register the route on the test Echo instance and create the request
with the encoded path (use e.Router()/e.ServeHTTP so Echo extracts and decodes
the "selector" param), or (B) if you keep using the context directly, pass the
encoded path into the request URL and use Echo's route matcher to populate
params instead of injecting tt.selector; apply the same change to the other
block at lines 153-156.

In `@internal/modelselectors/selectors.go`:
- Around line 197-213: The branch in ParseStoredParts that checks
splitFirst(selector) with rest == "" is redundant; simplify by removing that
early-return and rely on the existing splitFirst(selector) branch that returns
(providerName, model, ok) — when model is empty it already yields (prefix,""),
so delete the first if-block (the one inspecting rest == "") and keep the final
splitFirst branch to return providerName, model when ok is true, otherwise fall
back to returning "", selector.

In `@internal/pricingoverrides/service_test.go`:
- Around line 340-357: Test currently asserts on exact error substrings from
service.buildSnapshot which is brittle; change the test
(TestServiceBuildSnapshotRejectsDuplicateNormalizedSelectors) to assert the
error category/type instead of message text: use errors.Is or errors.As to check
for a sentinel or typed duplicate-selector error returned by buildSnapshot (or
assert IsValidationError(err) as appropriate), and if you need to verify
selector details, cast to the typed error and assert its Normalized/Original
fields rather than relying on strings.Contains.

In `@internal/pricingoverrides/service.go`:
- Around line 243-245: The function rollbackContext currently calls
context.WithTimeout(context.Background(), 30*time.Second) which intentionally
ignores the caller's context and potential cancellation; add a one-line
clarifying comment immediately above rollbackContext (referencing
rollbackContext, context.WithTimeout, context.Background) stating that using
context.Background is deliberate so cleanup/rollback can continue for up to 30s
even if the request context is cancelled and to warn future readers not to
replace it with a caller ctx.
- Around line 243-245: The rollbackContext function currently hard-codes
30*time.Second; change it to reuse the existing refreshTimeout constant (or if
you prefer, introduce a dedicated rollbackTimeout constant) to avoid
drift—update rollbackContext to call context.WithTimeout(context.Background(),
refreshTimeout) (or use the new rollbackTimeout) so the timeout is consistent
with the rest of the codebase.
- Around line 184-194: The refresh error path in refreshLocked handling returns
immediately when !existed, which can silently leave a persisted delete without
rollback because previous is zero-valued; add a short comment above the if
!existed branch explaining this is intentional (we have no prior snapshot to
restore) and that we choose not to attempt rollback when previous is unknown,
and then enhance the delete/refresh flow to treat a store-side ErrNotFound from
the Delete call as an explicit signal to also skip rollback (i.e., if
store.Delete returned ErrNotFound, log/annotate and return the refresh error
without attempting Upsert), keeping the existing rollback logic that uses
s.store.Upsert(rollbackCtx, previous) and
s.reconcileSnapshotAfterRollbackFailureLocked unchanged.

In `@internal/pricingoverrides/snapshot.go`:
- Around line 53-63: The switch on normalized.ScopeKind() in buildSnapshot
currently uses default to populate next.modelWide, which hides unknown/new
ScopeKind values; add an explicit case for modelselectors.ScopeModel that sets
next.modelWide[normalized.Model] = compiled, and change the default branch to
treat unknown scopes as an error by returning a descriptive error from
buildSnapshot (use the same return path as other errors). Reference the switch
handling in buildSnapshot and the variables next, normalized, and compiled when
making this change.

---

Outside diff comments:
In `@internal/gateway/inference_execute.go`:
- Around line 100-120: ExecuteEmbeddings currently passes resp.Model into
logUsage which skips admin pricing overrides keyed on the canonical requested
model; instead call usagePricingModel with the requested model and the response
model to resolve the correct pricing model (e.g., pricingModel :=
usagePricingModel(workflow, req.Model, resp.Model)) and pass that resolved
pricingModel into the logUsage callback used by
usage.ExtractFromEmbeddingResponse; keep EmbeddingResult.Model as resp.Model but
ensure logUsage uses the resolved pricing model.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8e74e597-d232-4bfe-bfac-c09126db99c0

📥 Commits

Reviewing files that changed from the base of the PR and between 1b91d68 and 45c4a96.

📒 Files selected for processing (13)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/admin/handler_model_overrides.go
  • internal/admin/handler_model_overrides_test.go
  • internal/admin/handler_model_pricing_overrides_test.go
  • internal/gateway/inference_execute.go
  • internal/modeloverrides/service_test.go
  • internal/modelselectors/selectors.go
  • internal/pricingoverrides/service.go
  • internal/pricingoverrides/service_test.go
  • internal/pricingoverrides/snapshot.go
  • internal/providers/registry_metadata_override_test.go
  • tools/openapi-postprocess.mjs

Comment thread docs/openapi.json Outdated
Comment thread docs/openapi.json
Comment thread internal/admin/handler_model_overrides.go
Comment thread internal/admin/handler_model_pricing_overrides_test.go Outdated
Comment thread internal/modelselectors/selectors.go
Comment thread internal/pricingoverrides/service_test.go
Comment thread internal/pricingoverrides/service.go
Comment thread internal/pricingoverrides/service.go
Comment thread internal/pricingoverrides/snapshot.go
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/modelselectors/selectors.go`:
- Around line 63-220: Add table-driven unit tests for selector
parsing/normalization and scope classification to cover NormalizeInput,
NormalizeInputWithProviderNames, NormalizeStored, ParseStoredParts,
ScopeKindFor, String, IsGlobal, ExactMatchKey and splitFirst: create a new
_test.go file with multiple test cases (including empty input, global "/",
provider-only "prov/", model-only "model", provider/model "prov/model",
slash-shaped model "org/model" with no provider in catalog, unknown provider,
and stored rows missing provider/model), assert expected Selector fields,
normalized Selector string, ExactMatchKey, and ScopeKind for each case, and
include provider catalog variants via ProviderNames/ProviderNames(catalog) to
exercise provider detection and fallback parsing. Ensure tests are table-driven,
cover error cases (validation errors), and include ParseStoredParts behavior for
legacy stored selectors.

In `@tools/openapi-postprocess.mjs`:
- Around line 162-170: The loop sets up_to_tokens.minimum = 0 which contradicts
tier validation that forbids zero; update the adjustment for
schema("pricingoverrides.Pricing") and the loop over
["core.ModelPricingTier","pricingoverrides.PricingTier"] so that
up_to_tokens.type remains "integer" but up_to_tokens.minimum = 1 (not 0) to
match the tier validation and prevent generating invalid payloads for the
up_to_tokens property.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1b6d98b8-35bb-4222-af4f-e399e82cbbc9

📥 Commits

Reviewing files that changed from the base of the PR and between 45c4a96 and f80d1c4.

📒 Files selected for processing (11)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/admin/handler_model_overrides.go
  • internal/admin/handler_model_pricing_overrides_test.go
  • internal/gateway/inference_execute.go
  • internal/modelselectors/selectors.go
  • internal/pricingoverrides/service.go
  • internal/pricingoverrides/service_test.go
  • internal/pricingoverrides/snapshot.go
  • internal/server/handlers_test.go
  • tools/openapi-postprocess.mjs
👮 Files not reviewed due to content moderation or server errors (2)
  • internal/pricingoverrides/service.go
  • docs/openapi.json

Comment on lines +63 to +220
// NormalizeInput validates and normalizes one user-supplied selector.
func NormalizeInput(catalog Catalog, raw string) (Selector, error) {
return NormalizeInputWithProviderNames(ProviderNames(catalog), raw)
}

// NormalizeInputWithProviderNames validates and normalizes one user-supplied selector.
func NormalizeInputWithProviderNames(providerNames []string, raw string) (Selector, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return Selector{}, NewValidationError("selector is required", nil)
}
if IsGlobal(raw) {
return Selector{Selector: "/"}, nil
}

providerNameSet := make(map[string]struct{}, len(providerNames))
for _, name := range providerNames {
name = strings.TrimSpace(name)
if name == "" {
continue
}
providerNameSet[name] = struct{}{}
}

var providerName, model string
if prefix, rest, ok := splitFirst(raw); ok {
if _, exists := providerNameSet[prefix]; exists {
providerName = prefix
model = rest
} else {
model = raw
}
} else {
model = raw
}

if providerName == "" && model == "" {
return Selector{}, NewValidationError("selector is required", nil)
}
if providerName != "" {
if _, exists := providerNameSet[providerName]; !exists {
return Selector{}, NewValidationError("unknown provider_name: "+providerName, nil)
}
}
return Selector{
Selector: String(providerName, model),
ProviderName: providerName,
Model: model,
}, nil
}

// NormalizeStored normalizes selector columns loaded from storage.
func NormalizeStored(selector, providerName, model string) (Selector, error) {
selector = strings.TrimSpace(selector)
providerName = strings.TrimSpace(providerName)
model = strings.TrimSpace(model)
globalSelector := IsGlobal(selector)

if selector == "" {
selector = String(providerName, model)
}
if selector == "" {
return Selector{}, NewValidationError("selector is required", nil)
}
if providerName == "" && model == "" && !globalSelector {
providerName, model = ParseStoredParts(selector)
}
if providerName == "" && model == "" && !globalSelector {
return Selector{}, NewValidationError("selector is required", nil)
}
if normalized := String(providerName, model); normalized != "" {
selector = normalized
}
return Selector{
Selector: selector,
ProviderName: providerName,
Model: model,
}, nil
}

// ProviderNames returns a copy of configured provider names from catalog.
func ProviderNames(catalog Catalog) []string {
if catalog == nil {
return nil
}
return append([]string(nil), catalog.ProviderNames()...)
}

// ScopeKindFor reports the normalized selector scope.
func ScopeKindFor(selector, providerName, model string) ScopeKind {
switch {
case IsGlobal(selector):
return ScopeGlobal
case strings.TrimSpace(providerName) != "" && strings.TrimSpace(model) != "":
return ScopeProviderModel
case strings.TrimSpace(providerName) != "":
return ScopeProvider
default:
return ScopeModel
}
}

// String returns the canonical selector string for normalized parts.
func String(providerName, model string) string {
providerName = strings.TrimSpace(providerName)
model = strings.TrimSpace(model)
switch {
case providerName != "" && model != "":
return providerName + "/" + model
case providerName != "":
return providerName + "/"
case model != "":
return model
default:
return ""
}
}

// IsGlobal reports whether raw selects every provider and model.
func IsGlobal(raw string) bool {
return strings.TrimSpace(raw) == "/"
}

// ExactMatchKey returns the lookup key used to index exact provider+model overrides.
// Returns "" when either side is missing — exact matching only applies to fully-qualified pairs.
func ExactMatchKey(providerName, model string) string {
providerName = strings.TrimSpace(providerName)
model = strings.TrimSpace(model)
if providerName == "" || model == "" {
return ""
}
return providerName + "/" + model
}

// ParseStoredParts splits a stored selector without a configured-provider catalog.
// This is a best-effort fallback for old rows. Stores should persist
// provider_name and model columns because slash-shaped model IDs cannot be
// distinguished from provider/model selectors without a provider catalog.
func ParseStoredParts(selector string) (providerName, model string) {
selector = strings.TrimSpace(selector)
if IsGlobal(selector) {
return "", ""
}
if providerName, model, ok := splitFirst(selector); ok {
return providerName, model
}
return "", selector
}

func splitFirst(value string) (prefix, rest string, ok bool) {
prefix, rest, ok = strings.Cut(value, "/")
if !ok {
return "", "", false
}
prefix = strings.TrimSpace(prefix)
rest = strings.TrimSpace(rest)
return prefix, rest, true
}
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add direct table-driven tests for selector normalization and scope classification.

This file is the core of selector parsing/normalization and currently has 0% patch coverage, so regressions in global/provider/model/exact behavior (especially slash-shaped IDs) can slip through.

As per coding guidelines, **/*_test.go: “Add or update tests for behavior changes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/modelselectors/selectors.go` around lines 63 - 220, Add table-driven
unit tests for selector parsing/normalization and scope classification to cover
NormalizeInput, NormalizeInputWithProviderNames, NormalizeStored,
ParseStoredParts, ScopeKindFor, String, IsGlobal, ExactMatchKey and splitFirst:
create a new _test.go file with multiple test cases (including empty input,
global "/", provider-only "prov/", model-only "model", provider/model
"prov/model", slash-shaped model "org/model" with no provider in catalog,
unknown provider, and stored rows missing provider/model), assert expected
Selector fields, normalized Selector string, ExactMatchKey, and ScopeKind for
each case, and include provider catalog variants via
ProviderNames/ProviderNames(catalog) to exercise provider detection and fallback
parsing. Ensure tests are table-driven, cover error cases (validation errors),
and include ParseStoredParts behavior for legacy stored selectors.

Comment thread tools/openapi-postprocess.mjs
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/openapi-postprocess.mjs`:
- Around line 161-170: The OpenAPI schema modification in
applyPricingSchemaConstraints sets a minimum for up_to_tokens but not for
up_to_mtok, allowing zero thresholds the backend rejects; update
applyPricingSchemaConstraints to locate the up_to_mtok property on the same
schemas checked (e.g., in the loop over "core.ModelPricingTier" and
"pricingoverrides.PricingTier") and set up_to_mtok.type = "integer" and
up_to_mtok.minimum = 1 (mirror the changes applied to up_to_tokens) so both
fields are constrained to >=1.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9df8c7b6-b9bb-482e-9bf8-a6e4236bca86

📥 Commits

Reviewing files that changed from the base of the PR and between 45c4a96 and bb3bd1b.

📒 Files selected for processing (12)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/admin/handler_model_overrides.go
  • internal/admin/handler_model_pricing_overrides_test.go
  • internal/gateway/inference_execute.go
  • internal/modelselectors/selectors.go
  • internal/modelselectors/selectors_test.go
  • internal/pricingoverrides/service.go
  • internal/pricingoverrides/service_test.go
  • internal/pricingoverrides/snapshot.go
  • internal/server/handlers_test.go
  • tools/openapi-postprocess.mjs
👮 Files not reviewed due to content moderation or server errors (4)
  • cmd/gomodel/docs/docs.go
  • internal/pricingoverrides/snapshot.go
  • internal/admin/handler_model_pricing_overrides_test.go
  • internal/pricingoverrides/service.go

Comment on lines +161 to +170
function applyPricingSchemaConstraints() {
schema("pricingoverrides.Pricing").minProperties = 1;
for (const name of ["core.ModelPricingTier", "pricingoverrides.PricingTier"]) {
const upToTokens = schema(name).properties?.up_to_tokens;
if (!upToTokens) {
throw new Error(`missing up_to_tokens property on schema: ${name}`);
}
upToTokens.type = "integer";
upToTokens.minimum = 1;
}
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 | ⚡ Quick win

Align up_to_mtok schema bounds with tier validation

applyPricingSchemaConstraints() enforces up_to_tokens.minimum = 1, but leaves up_to_mtok unconstrained. The service rejects zero thresholds, so current OpenAPI still permits payloads the backend will reject.

♻️ Proposed fix
 function applyPricingSchemaConstraints() {
   schema("pricingoverrides.Pricing").minProperties = 1;
   for (const name of ["core.ModelPricingTier", "pricingoverrides.PricingTier"]) {
-    const upToTokens = schema(name).properties?.up_to_tokens;
+    const tierProps = schema(name).properties || {};
+    const upToTokens = tierProps.up_to_tokens;
     if (!upToTokens) {
       throw new Error(`missing up_to_tokens property on schema: ${name}`);
     }
     upToTokens.type = "integer";
     upToTokens.minimum = 1;
+    if (tierProps.up_to_mtok) {
+      tierProps.up_to_mtok.minimum = 1;
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/openapi-postprocess.mjs` around lines 161 - 170, The OpenAPI schema
modification in applyPricingSchemaConstraints sets a minimum for up_to_tokens
but not for up_to_mtok, allowing zero thresholds the backend rejects; update
applyPricingSchemaConstraints to locate the up_to_mtok property on the same
schemas checked (e.g., in the loop over "core.ModelPricingTier" and
"pricingoverrides.PricingTier") and set up_to_mtok.type = "integer" and
up_to_mtok.minimum = 1 (mirror the changes applied to up_to_tokens) so both
fields are constrained to >=1.

@SantiagoDePolonia SantiagoDePolonia merged commit 91ac883 into main May 7, 2026
19 checks passed
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.

2 participants