Skip to content

recalculate cost fixes - #5669

Merged
akshaydeo merged 1 commit into
devfrom
07-30-recalculate_cost_fixes
Jul 30, 2026
Merged

recalculate cost fixes#5669
akshaydeo merged 1 commit into
devfrom
07-30-recalculate_cost_fixes

Conversation

@akshaydeo

@akshaydeo akshaydeo commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Cost recomputation was systematically mis-pricing requests due to three compounding issues: (1) the SearchLogs projection used for billing omitted modality output payloads and, on hybrid object-storage-backed stores, returned rows whose token_usage was blanked at write time — causing DeserializeFields to rebuild a lossy stub where PromptTokens is inclusive of cache buckets, inflating cache-heavy requests by 2–4x; (2) the Log table had no columns for the served billing tier (service_tier, speed, inference_geo), so every row repriced at standard rates regardless of what was actually served; and (3) several modality-specific pricing inputs (OCR pages, 1h cache-write split, server-side fallback model) were dropped during response reconstruction.

A user observed approximately 3x overbilling in production on a cache-heavy Anthropic request.

Changes

  • New SearchLogsForBilling interface method on LogStore that selects the full billing projection (including modality output payloads) and, on HybridLogStore, concurrently hydrates offloaded payloads from object storage. Rows whose payloads cannot be recovered (content-hidden, missing object) are returned as an unpriceable ID list rather than silently handed back as stubs.

  • billingSelectColumns / billingPayloadColumns added to RDBLogStore. The list projection (listSelectColumns) deliberately stays free of unbounded output blobs to avoid reintroducing the Cloud Run 32 MB body-limit failure. The billing projection extends it with speech_output, transcription_output, image_generation_output, video_generation_output, and ocr_output.

  • IsUsageDegraded flag on Log. DeserializeFields marks the stub it builds from denormalized columns as degraded; calculateCostForLog refuses to price a degraded row and returns errPricingInputsUnavailable. The flag clears when a real token_usage payload is subsequently parsed (the hybrid hydration path calls DeserializeFields twice).

  • New denormalized columns: cached_write_tokens, service_tier, speed, inference_geo on the Log table, added via migrationAddBillingFidelityColumns. cached_write_tokens is backfilled from existing token_usage JSON via ensureBillingFidelityBackfill (deferred out of the migration to avoid blocking pod startup). The tier columns cannot be backfilled — pre-migration rows reprice at standard rates.

  • applyServedTierToEntry captures the served tier at write time from both the response envelope (non-streaming) and the usage struct (streaming, where no envelope is accumulated). Called from applyNonStreamingOutputToEntry, applyStreamingOutputToEntry, and applyRealtimeOutputToEntry.

  • servedTierFromLog / buildResponseForRequestType restore the tier onto the reconstructed BifrostResponse so CalculateCost's tierFromResponse sees the correct multipliers.

  • OCR billing fix: resp.OCRResponse now has Pages, UsageInfo, and DocumentAnnotation patched back in from OCROutputParsed. Previously the reconstructed response reported zero pages and the entire request priced to nothing.

  • 1h cache-write split fix: CachedWriteTokenDetails is now forwarded through buildResponseForRequestType for the Responses path, preventing 1h cache writes from billing at the cheaper 5m rate.

  • Server-side fallback model fix: logEntry.ServerSideFallbackModel is now set on RoutingInfo.ServerSideFallbackModel so resolvePricing prices at the model that actually ran.

  • Alias-without-canonical fix: ResolvedKeyAlias.ModelID is now populated whenever logEntry.Alias is set, independent of whether a canonical name exists, so per-deployment override pricing resolves correctly.

  • Unpriceable counter added to CostRecalcJobMeta, RecalculateCostResult, and the API status struct, distinguishing "nothing to charge" from "refused to write a number known to be wrong".

  • costRecalcBatchSize reduced from 1000 to 200 to bound the object-storage fan-out (one Get per offloaded row per batch).

  • waitForOffload test helper waits for both the object upload and the has_object DB flag commit, fixing a race in TestHybrid_ContentHiddenStripsDBRowAndSkipsHydration where processUpload writes the flag after the Put.

Type of change

  • Bug fix
  • Feature

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Plugins

How to test

go test ./framework/logstore/... ./plugins/logging/...

Key test files:

  • framework/logstore/billingprojection_test.go — projection separation and end-to-end modality output delivery
  • framework/logstore/hybridbilling_test.go — hydration, content-hidden reporting, fetch-failure reporting, non-offloaded passthrough
  • plugins/logging/costfidelity_test.go — cache breakdown parity, degraded-usage refusal, tier preservation, 1h cache-write pricing, server-side fallback model, unpriceable accounting

Breaking changes

  • No

The SearchLogsForBilling method is additive to the LogStore interface. Existing fakeRecalcStore test doubles require a SearchLogsForBilling implementation (see costrecalc_test.go for the pattern: delegate to SearchLogs and return a nil unpriceable list).

Security considerations

None. No auth, secrets, or PII surface changes. The new columns store provider-echoed tier strings ("priority", "flex", "fast", "us") that are already present in the response payloads.

Checklist

  • I added/updated tests where appropriate
  • I verified builds succeed (Go and UI)

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae03391e-26bf-4fec-9fe6-b537c7d82d48

📥 Commits

Reviewing files that changed from the base of the PR and between 69305f7 and 78d8e96.

📒 Files selected for processing (24)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • framework/streaming/accumulator.go
  • framework/streaming/accumulator_test.go
  • framework/streaming/chat.go
  • framework/streaming/responses.go
  • framework/streaming/responses_test.go
  • framework/streaming/types.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • plugins/logging/operations_test.go
  • transports/bifrost-http/handlers/logging.go

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added billing-aware log search plus chunked offloaded-pricing hydration, with optional backfill to speed up subsequent runs.
    • Persisted served-billing metadata (service tier, speed, inference geo) to improve cost recomputation accuracy.
    • Added “unpriceable” counters to cost-recalculation results and job status.
  • Bug Fixes
    • Improved SQL projections and billing projections to precisely select needed pricing inputs and gate payload access by object type.
    • Fixed fidelity issues across routing/model/tier handling, OCR reconstruction, cache breakdown, and degraded/unfetchable scenarios.
    • Ensured served-tier is retained through streaming accumulation and written onto log entries.
  • Tests
    • Expanded projection, hydration-gating, backfill convergence, and end-to-end cost-fidelity regression coverage.

Walkthrough

Billing log retrieval separates list and pricing projections, hybrid stores hydrate missing pricing inputs, cost reconstruction preserves served metadata and modality details, and recalculation reports rows whose pricing inputs remain unavailable.

Changes

Billing fidelity pipeline

Layer / File(s) Summary
Billing storage and projections
framework/logstore/{migrations.go,payload.go,rdb.go,store.go,tables.go}, framework/logstore/billingprojection_test.go
Billing metadata, degraded-usage provenance, separate projections, payload lifecycle helpers, and object-type-gated billing selection are added and tested.
Hybrid billing hydration
framework/logstore/{hybrid.go,*billing*_test.go,hybrid_test.go,contenthidden_test.go}
Hybrid billing searches identify missing inputs, hydrate eligible offloaded rows, avoid redundant fetches, and return unpriceable IDs.
Streaming served metadata
framework/streaming/{types.go,chat.go,responses.go,accumulator.go}, plugins/logging/operations_test.go
Streaming chunks preserve served service tiers through usage-only chunks and logging persists the resulting metadata.
Tier-aware pricing reconstruction
plugins/logging/{operations.go,costfidelity_test.go}
Served tiers, cache details, fallback models, and modality-specific fields are preserved during ingestion and cost reconstruction.
Recalculation accounting
plugins/logging/{costrecalc.go,main.go,costrecalc_test.go}, transports/bifrost-http/handlers/logging.go
Cost recalculation uses bounded billing hydration, releases payloads, tracks unpriceable rows, and exposes the counter in job metadata and status responses.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RecalculateCosts
  participant LogStore
  participant ObjectStore
  participant PricingManager
  RecalculateCosts->>LogStore: SearchLogsForBilling
  RecalculateCosts->>LogStore: HydrateBillingChunk
  LogStore->>ObjectStore: fetch missing pricing inputs
  ObjectStore-->>LogStore: return hydrated payload
  RecalculateCosts->>PricingManager: reconstruct and calculate cost
  PricingManager-->>RecalculateCosts: return billing outcome
Loading

Possibly related PRs

  • maximhq/bifrost#4931: Both changes modify calculateCostForLog pricing-input requirements and cost computation.
  • maximhq/bifrost#5387: Cost recomputation uses fallback-model metadata populated by the related usage conversion path.

Suggested reviewers: impoiler, pratham-mishra04, roroghost17

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the work but too generic to convey the main change clearly. Use a specific title like "Fix billing cost recomputation and billing projections".
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the template well and includes summary, changes, testing, impact, security, and checklist sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 07-30-recalculate_cost_fixes

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

akshaydeo commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@akshaydeo
akshaydeo marked this pull request as ready for review July 29, 2026 21:23

@coderabbitai coderabbitai Bot left a comment

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.

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 `@framework/logstore/hybrid.go`:
- Around line 656-667: Preserve the inner store’s unpriceable rows in
HybridLogStore.SearchLogsForBilling by capturing the second return value from
h.inner.SearchLogsForBilling and initializing the local unpriceable slice from
it before hydration; retain the existing mutex and append behavior for rows
discovered locally.

In `@framework/logstore/migrations.go`:
- Around line 3849-3868: Update ensureBillingFidelityBackfill’s batch filter to
use pg_input_is_valid(token_usage, 'jsonb') before any token_usage::jsonb
expressions, while retaining the existing non-empty and object-shape conditions
and cached_write_tokens criteria. This must prevent malformed legacy JSON rows
from aborting the batch.
🪄 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 Plus

Run ID: f30bd0fa-d392-40aa-9060-d0f8ddf871a4

📥 Commits

Reviewing files that changed from the base of the PR and between c6f0dcd and 55f6b7c.

📒 Files selected for processing (16)
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/postgres.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go

Comment thread framework/logstore/hybrid.go Outdated
Comment thread framework/logstore/migrations.go Outdated
@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch from 55f6b7c to a49c418 Compare July 29, 2026 23:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
transports/bifrost-http/handlers/logging.go (1)

2001-2017: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

omitempty makes unpriceable inconsistent with the sibling counters.

Total, Processed, Updated, and Skipped are always emitted; Unpriceable disappears when zero, so clients see undefined rather than 0 for the common case. Dropping omitempty keeps the counter set uniform.

♻️ Proposed change
-	Unpriceable int        `json:"unpriceable,omitempty"`
+	Unpriceable int        `json:"unpriceable"`
🤖 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 `@transports/bifrost-http/handlers/logging.go` around lines 2001 - 2017, Update
the Unpriceable field in the response struct to remove omitempty from its JSON
tag, keeping unpriceable serialized as 0 alongside the other counter fields.
plugins/logging/operations.go (1)

1506-1541: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Release the chunk's payloads on the hydration-error path too.

return nil, fmt.Errorf("failed to hydrate pricing inputs: ...") leaves the partially hydrated chunk's payloads attached to batch. Callers abandon the batch on error so GC reclaims it, but a defer-free early return makes the "at most one chunk resident" invariant depend on the caller rather than on this function.

♻️ Optional tightening
 		unpriceable, err := p.store.HydrateBillingChunk(ctx, chunk)
 		if err != nil {
+			logstore.ReleaseBillingPayloads(chunk)
 			return nil, fmt.Errorf("failed to hydrate pricing inputs: %w", err)
 		}
🤖 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 `@plugins/logging/operations.go` around lines 1506 - 1541, Update
priceLogsInChunks so the chunk passed to HydrateBillingChunk is released with
logstore.ReleaseBillingPayloads before returning on hydration error. Preserve
the existing wrapped error and normal-path release, ensuring every hydrated
chunk is released within this function.
framework/logstore/hybrid.go (1)

714-757: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

cache_debug will trigger a fetch for nearly every offloaded row under the default config.

The doc says consulting the exclusion set removes the "empty means offloaded or never written" ambiguity, but it only does so when the column is in the exclusion set. With object_storage_exclude_fields unset (the default), a row with no semantic cache has an empty cache_debug and is therefore classified as needing hydration. In practice token_usage is offloaded in that same configuration so the fetch happens regardless, and billingPayloadsHydrated prevents a repeat, so this is not a live regression — but the comment overstates the precision of the gate. Worth softening the wording so a future reader doesn't rely on a guarantee that only holds for excluded columns.

🤖 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 `@framework/logstore/hybrid.go` around lines 714 - 757, Soften the explanatory
comment in billingRowNeedsHydration and its missing helper to clarify that the
exclusion set disambiguates empty values only for columns explicitly listed
there; unlisted empty columns may still represent either absent data or
offloaded data. Keep the existing hydration 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.

Nitpick comments:
In `@framework/logstore/hybrid.go`:
- Around line 714-757: Soften the explanatory comment in
billingRowNeedsHydration and its missing helper to clarify that the exclusion
set disambiguates empty values only for columns explicitly listed there;
unlisted empty columns may still represent either absent data or offloaded data.
Keep the existing hydration logic unchanged.

In `@plugins/logging/operations.go`:
- Around line 1506-1541: Update priceLogsInChunks so the chunk passed to
HydrateBillingChunk is released with logstore.ReleaseBillingPayloads before
returning on hydration error. Preserve the existing wrapped error and
normal-path release, ensuring every hydrated chunk is released within this
function.

In `@transports/bifrost-http/handlers/logging.go`:
- Around line 2001-2017: Update the Unpriceable field in the response struct to
remove omitempty from its JSON tag, keeping unpriceable serialized as 0
alongside the other counter fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 33fc80a9-532e-4a36-b328-a61f658078e6

📥 Commits

Reviewing files that changed from the base of the PR and between 55f6b7c and a49c418.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/logging/main.go

@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch 2 times, most recently from 7d07f11 to 32db51f Compare July 29, 2026 23:24

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (4)
framework/logstore/billinghydrationgate_test.go (1)

24-32: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

gets is appended and read without synchronization.

The hybrid store runs background upload workers, and the test goroutine reads objStore.gets while they are alive. No worker calls Get today, so this doesn't race yet, but it is one hydration call from a goroutine away from a -race failure — and the fix is three lines.

🔒 Proposed fix
 type countingObjectStore struct {
 	*objectstore.InMemoryObjectStore
-	gets []string
+	mu   sync.Mutex
+	gets []string
 }
 
 func (s *countingObjectStore) Get(ctx context.Context, key string) ([]byte, error) {
-	s.gets = append(s.gets, key)
+	s.mu.Lock()
+	s.gets = append(s.gets, key)
+	s.mu.Unlock()
 	return s.InMemoryObjectStore.Get(ctx, key)
 }
+
+func (s *countingObjectStore) getKeys() []string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return slices.Clone(s.gets)
+}
🤖 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 `@framework/logstore/billinghydrationgate_test.go` around lines 24 - 32,
Protect countingObjectStore.gets with a mutex: add synchronization to the
struct, lock around the append in Get, and use the same lock whenever the test
reads gets. Keep the existing object-store delegation unchanged.
framework/logstore/hybrid.go (1)

802-811: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

DeserializeFields runs twice per hydrated row.

MergePayloadFromJSON already deserializes every merged field (and rebuilds ContentSummary) before pruneUnrequestedPayloadFields narrows the set, then line 809 parses again. On an image-generation row that means the full base64 payload is unmarshalled into ImageData before being discarded — the transient spike the chunk size exists to bound. Not a correctness problem, but pruning before the first parse would avoid it if MergePayloadFromJSON gains a field-filtered variant.

🤖 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 `@framework/logstore/hybrid.go` around lines 802 - 811, Avoid the duplicate
deserialization in the hydration flow around MergePayloadFromJSON and
DeserializeFields: add or use a field-filtered merge path that applies
pruneUnrequestedPayloadFields before deserializing merged fields. Preserve
ContentSummary and TokenUsageParsed hydration while ensuring unrequested fields
such as ImageData are never unmarshalled before being discarded.
framework/logstore/store.go (1)

43-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc refers to an unpriceable return that no longer exists.

The signature now returns BillingHydrationResult; the prose still describes a bare unpriceable slice. Worth naming the field so implementers of this interface aren't looking for a second return value.

📝 Proposed doc tweak
-	// unpriceable carries the IDs whose inputs could not be recovered — a failed object
-	// fetch, or a content-hidden row whose payload is never fetched back by design.
+	// BillingHydrationResult.Unpriceable carries the IDs whose inputs could not be
+	// recovered — a failed object fetch, or a content-hidden row whose payload is
+	// never fetched back by design.
🤖 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 `@framework/logstore/store.go` around lines 43 - 55, Update the
HydrateBillingChunk documentation to refer explicitly to the unpriceable field
on BillingHydrationResult rather than implying a separate return value. Preserve
the existing explanation of which IDs are included and how callers must handle
them.
plugins/logging/costrecalc_test.go (1)

410-443: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the recorded backfill stays empty.

backfilled is recorded but never checked, so the documented invariant at Line 57 ("should never be reached here") isn't enforced — a regression that backfills rows the store never hydrated would pass silently.

♻️ Suggested addition
 	if total != rows {
 		t.Fatalf("chunks covered %d rows, want every one of %d: %v", total, rows, store.hydrateChunkSizes)
 	}
+	if len(store.backfilled) != 0 {
+		t.Fatalf("nothing was hydrated, so nothing may be written back; got backfill calls %v", store.backfilled)
+	}
 }
🤖 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 `@plugins/logging/costrecalc_test.go` around lines 410 - 443, Update
TestRunCostRecalcJob_HydratesInBoundedChunks to assert that the recorded
backfilled collection remains empty after the job completes, failing the test if
any rows were backfilled without hydration.
🤖 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 `@framework/logstore/hybrid.go`:
- Around line 738-746: Update the hydration decision around vouched and
billingPayloadColumnFor so token-billed offloaded rows with denormalized usage
counters indicating nothing to price are treated as already resolved, including
rows with empty token_usage. Keep modality objects eligible for fetching by
applying this guard only when billingPayloadColumnFor(l.Object) is empty, and
preserve existing backfill behavior for rows whose counters indicate billable
usage.

In `@framework/logstore/hybridbilling_test.go`:
- Around line 123-126: Complete the BillingHydrationResult migration: in
framework/logstore/hybridbilling_test.go at lines 91-93, 123-126, and 149-153,
rename the result variable from unpriceable and assert against its Unpriceable
field; at lines 180-182 and 231-234, inspect the Unpriceable and Hydrated fields
respectively instead of asserting on the struct; update the
framework/logstore/store.go lines 43-55 documentation to reference
BillingHydrationResult.Unpriceable rather than a standalone unpriceable return
value.

In `@plugins/logging/operations.go`:
- Around line 1610-1621: Move the provably-zero direct-cache-hit handling ahead
of the IsUsageDegraded gate in the pricing flow, so degraded usage rows with a
known zero cost return successfully without being rejected. Preserve the
existing errPricingInputsUnavailable behavior for degraded rows that still
require pricing.

---

Nitpick comments:
In `@framework/logstore/billinghydrationgate_test.go`:
- Around line 24-32: Protect countingObjectStore.gets with a mutex: add
synchronization to the struct, lock around the append in Get, and use the same
lock whenever the test reads gets. Keep the existing object-store delegation
unchanged.

In `@framework/logstore/hybrid.go`:
- Around line 802-811: Avoid the duplicate deserialization in the hydration flow
around MergePayloadFromJSON and DeserializeFields: add or use a field-filtered
merge path that applies pruneUnrequestedPayloadFields before deserializing
merged fields. Preserve ContentSummary and TokenUsageParsed hydration while
ensuring unrequested fields such as ImageData are never unmarshalled before
being discarded.

In `@framework/logstore/store.go`:
- Around line 43-55: Update the HydrateBillingChunk documentation to refer
explicitly to the unpriceable field on BillingHydrationResult rather than
implying a separate return value. Preserve the existing explanation of which IDs
are included and how callers must handle them.

In `@plugins/logging/costrecalc_test.go`:
- Around line 410-443: Update TestRunCostRecalcJob_HydratesInBoundedChunks to
assert that the recorded backfilled collection remains empty after the job
completes, failing the test if any rows were backfilled without hydration.
🪄 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 Plus

Run ID: 7c9a07df-1600-479c-873e-e75d18e41976

📥 Commits

Reviewing files that changed from the base of the PR and between a49c418 and 7d07f11.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go

Comment thread framework/logstore/hybrid.go
Comment thread framework/logstore/hybridbilling_test.go Outdated
Comment thread plugins/logging/operations.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
framework/logstore/hybrid.go (1)

765-781: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive this from billingPayloadColumns rather than a parallel switch. The column set lives in rdb.go; adding a key there without touching this switch silently returns "", which makes the gate fetch that row on every pass forever. A map[string]func(*Log) string (or a getter beside the existing map) keeps the two in one place.

🤖 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 `@framework/logstore/hybrid.go` around lines 765 - 781, Update
billingPayloadColumnValue to derive payload accessors from the existing
billingPayloadColumns definition in rdb.go instead of maintaining a parallel
switch. Add or reuse a getter map keyed by column name, including the current
Log fields, and return the existing empty fallback only for unknown keys so
newly added billing columns cannot silently remain unprocessed.
plugins/logging/costrecalc_test.go (1)

57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the documented "never backfilled" invariant.

The comment states BulkBackfillBillingPayloads should never be reached with this fake, and backfilled records it, but no test checks it. Adding if len(store.backfilled) != 0 { ... } to the new test would pin that nothing is written back when nothing was hydrated.

Also applies to: 429-443

🤖 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 `@plugins/logging/costrecalc_test.go` around lines 57 - 62, Update the new test
covering the fakeRecalcStore flow to assert that store.backfilled remains empty
after execution. Use the existing backfilled tracking field and fail the test
when its length is nonzero, preserving the documented invariant that
BulkBackfillBillingPayloads is never reached.
plugins/logging/operations.go (1)

1494-1568: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Backfill accumulated so far is dropped if a later chunk fails to hydrate.

priceLogsInChunks returns early on HydrateBillingChunk error, discarding the backfill map built from earlier chunks. Those fetches then get paid again on the next run. Flushing what you have before returning would keep the self-healing property.

♻️ Optional: flush before propagating the error
 		hydration, err := p.store.HydrateBillingChunk(ctx, chunk)
 		if err != nil {
+			p.flushBillingBackfill(ctx, backfill)
 			return nil, fmt.Errorf("failed to hydrate pricing inputs: %w", err)
 		}
🤖 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 `@plugins/logging/operations.go` around lines 1494 - 1568, Update
priceLogsInChunks so a HydrateBillingChunk failure flushes any accumulated
backfill entries through BulkBackfillBillingPayloads before returning the
hydration error. Preserve the existing non-fatal backfill behavior and avoid
attempting the write when backfill is empty.
framework/logstore/billingprojection_test.go (1)

108-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Substring matching can misfire as the projection evolves.

strings.Contains(cols, unused) matches any column whose name embeds one of these tokens (e.g. responses_input_history also satisfies the input_history entry, and a future metadata_* column would trip the metadata entry). Consider reusing the tokenized containsColumn helper (plus an AS <name> check) so the assertion fails only on a genuine selection.

🤖 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 `@framework/logstore/billingprojection_test.go` around lines 108 - 126, Update
TestBillingProjectionOmitsColumnsPricingNeverReads to use the token-aware
containsColumn helper instead of strings.Contains, including its AS <name>
validation, so each forbidden entry matches only an actual selected column
rather than a substring of another column name.
🤖 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.

Nitpick comments:
In `@framework/logstore/billingprojection_test.go`:
- Around line 108-126: Update TestBillingProjectionOmitsColumnsPricingNeverReads
to use the token-aware containsColumn helper instead of strings.Contains,
including its AS <name> validation, so each forbidden entry matches only an
actual selected column rather than a substring of another column name.

In `@framework/logstore/hybrid.go`:
- Around line 765-781: Update billingPayloadColumnValue to derive payload
accessors from the existing billingPayloadColumns definition in rdb.go instead
of maintaining a parallel switch. Add or reuse a getter map keyed by column
name, including the current Log fields, and return the existing empty fallback
only for unknown keys so newly added billing columns cannot silently remain
unprocessed.

In `@plugins/logging/costrecalc_test.go`:
- Around line 57-62: Update the new test covering the fakeRecalcStore flow to
assert that store.backfilled remains empty after execution. Use the existing
backfilled tracking field and fail the test when its length is nonzero,
preserving the documented invariant that BulkBackfillBillingPayloads is never
reached.

In `@plugins/logging/operations.go`:
- Around line 1494-1568: Update priceLogsInChunks so a HydrateBillingChunk
failure flushes any accumulated backfill entries through
BulkBackfillBillingPayloads before returning the hydration error. Preserve the
existing non-fatal backfill behavior and avoid attempting the write when
backfill is empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 890d0045-660b-4f99-bb98-6eed2e03f6b9

📥 Commits

Reviewing files that changed from the base of the PR and between 7d07f11 and 32db51f.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go

@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch from 32db51f to e27f1d6 Compare July 30, 2026 07:43

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (5)
framework/logstore/store.go (1)

43-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc refers to a return name that no longer exists.

HydrateBillingChunk returns BillingHydrationResult; the paragraph still talks about "unpriceable carries the IDs". Rename to result.Unpriceable so implementers reading the contract aren't looking for a named result.

🤖 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 `@framework/logstore/store.go` around lines 43 - 55, The HydrateBillingChunk
contract documentation refers to a nonexistent named return. Update the
unpriceable paragraph to identify the returned field as result.Unpriceable,
preserving the existing description of which IDs it contains and how callers
must handle them.
framework/logstore/billinghydrationgate_test.go (1)

24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider guarding gets with a mutex.

Today only the sequential HydrateBillingChunk loop calls Get, so this is race-free. But FindByID/SearchLogs on the hybrid store also hydrate, and the list path can fan out — the first test that touches those under -race will flag this append.

🤖 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 `@framework/logstore/billinghydrationgate_test.go` around lines 24 - 32, The
countingObjectStore.Get method records keys in the shared gets slice without
synchronization. Add a mutex to countingObjectStore and guard the gets append in
Get, preserving the existing object-store retrieval behavior and ensuring
concurrent hydration through FindByID or SearchLogs is race-safe.
plugins/logging/costrecalc_test.go (1)

57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The stated invariant isn't asserted.

The comment says backfill "should never be reached here", but nothing checks it. Since backfilled is already recorded, adding if len(store.backfilled) != 0 to the chunk test would turn the comment into a guarantee.

🤖 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 `@plugins/logging/costrecalc_test.go` around lines 57 - 62, Add an assertion to
the relevant chunk test that the fake store’s recorded backfill calls remain
empty, using the backfilled field populated by
fakeRecalcStore.BulkBackfillBillingPayloads. This should enforce the stated
invariant that BulkBackfillBillingPayloads is never reached.
plugins/logging/operations.go (1)

710-717: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Realtime tier capture is gated on usage being present.

applyServedTierToEntry(entry, result, bifrostUsage) sits inside if usage := result.ResponsesResponse.Usage; usage != nil, so a realtime turn that reports a served tier but no usage never records service_tier / speed / inference_geo. The helper already tolerates a nil usage argument, so it can be hoisted out of the block.

♻️ Suggested hoist
 	if usage := result.ResponsesResponse.Usage; usage != nil {
 		bifrostUsage := usage.ToBifrostLLMUsage()
 		entry.TokenUsageParsed = bifrostUsage
 		entry.PromptTokens = bifrostUsage.PromptTokens
 		entry.CompletionTokens = bifrostUsage.CompletionTokens
 		entry.TotalTokens = bifrostUsage.TotalTokens
-		applyServedTierToEntry(entry, result, bifrostUsage)
+		applyServedTierToEntry(entry, result, bifrostUsage)
+	} else {
+		applyServedTierToEntry(entry, result, nil)
 	}
🤖 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 `@plugins/logging/operations.go` around lines 710 - 717, Move
applyServedTierToEntry outside the usage != nil conditional so realtime
served-tier metadata is captured even when ResponsesResponse.Usage is absent.
Keep the existing usage-to-token-field assignments inside the conditional, and
pass the available bifrostUsage value (nil when usage is unavailable) to the
helper.
framework/logstore/billingprojection_test.go (1)

108-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Substring matching here can false-fail on future column names.

strings.Contains(cols, "metadata") (and "speech_input", etc.) will also match any new column that embeds these names (e.g. metadata_hash, speech_input_ref), failing the test for a column pricing legitimately does not read. Reusing the token-level containsColumn (plus an AS <name> check) would keep the intent without the substring coupling.

🤖 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 `@framework/logstore/billingprojection_test.go` around lines 108 - 126, The
test TestBillingProjectionOmitsColumnsPricingNeverReads should match complete
selected column names rather than using strings.Contains, which can match
unrelated names such as metadata_hash. Reuse the existing token-level
containsColumn helper and include the AS <name> check when validating each
unused column.
🤖 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 `@framework/logstore/hybrid.go`:
- Around line 802-811: Update the hydration flow around MergePayloadFromJSON and
pruneUnrequestedPayloadFields to avoid fully deserializing and summarizing
payload data before unrequested fields are removed. Prune or narrow the initial
merge so the billing path restores only the fields needed for billing, then run
log.DeserializeFields() once on the retained payload.

---

Nitpick comments:
In `@framework/logstore/billinghydrationgate_test.go`:
- Around line 24-32: The countingObjectStore.Get method records keys in the
shared gets slice without synchronization. Add a mutex to countingObjectStore
and guard the gets append in Get, preserving the existing object-store retrieval
behavior and ensuring concurrent hydration through FindByID or SearchLogs is
race-safe.

In `@framework/logstore/billingprojection_test.go`:
- Around line 108-126: The test
TestBillingProjectionOmitsColumnsPricingNeverReads should match complete
selected column names rather than using strings.Contains, which can match
unrelated names such as metadata_hash. Reuse the existing token-level
containsColumn helper and include the AS <name> check when validating each
unused column.

In `@framework/logstore/store.go`:
- Around line 43-55: The HydrateBillingChunk contract documentation refers to a
nonexistent named return. Update the unpriceable paragraph to identify the
returned field as result.Unpriceable, preserving the existing description of
which IDs it contains and how callers must handle them.

In `@plugins/logging/costrecalc_test.go`:
- Around line 57-62: Add an assertion to the relevant chunk test that the fake
store’s recorded backfill calls remain empty, using the backfilled field
populated by fakeRecalcStore.BulkBackfillBillingPayloads. This should enforce
the stated invariant that BulkBackfillBillingPayloads is never reached.

In `@plugins/logging/operations.go`:
- Around line 710-717: Move applyServedTierToEntry outside the usage != nil
conditional so realtime served-tier metadata is captured even when
ResponsesResponse.Usage is absent. Keep the existing usage-to-token-field
assignments inside the conditional, and pass the available bifrostUsage value
(nil when usage is unavailable) to the helper.
🪄 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 Plus

Run ID: a6bb2d58-b5c2-4f50-b8d7-de8d4d22e900

📥 Commits

Reviewing files that changed from the base of the PR and between 32db51f and e27f1d6.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • framework/logstore/hybridbilling_test.go

Comment thread framework/logstore/hybrid.go
@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch from e27f1d6 to ab0f06b Compare July 30, 2026 08:13

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
plugins/logging/costrecalc_test.go (1)

57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

backfilled is recorded but never asserted, so the documented "never reached" invariant isn't pinned.

Adding if len(store.backfilled) != 0 { ... } to TestRunCostRecalcJob_HydratesInBoundedChunks would make the comment enforceable — otherwise a regression that backfills rows nothing hydrated goes unnoticed.

🤖 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 `@plugins/logging/costrecalc_test.go` around lines 57 - 62, Update
TestRunCostRecalcJob_HydratesInBoundedChunks to assert that the fakeRecalcStore
backfilled collection remains empty after the job runs. Use the existing
backfilled field to enforce the documented invariant that
BulkBackfillBillingPayloads is never invoked when nothing is hydrated.
framework/logstore/rdb.go (1)

743-758: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Single transaction wrapping N row updates.

Fine at current batch sizes, but the loop issues one UPDATE per id inside one transaction, so the write lock footprint grows linearly with the backfill map. Consider chunking (or a CASE-based multi-row update) if batch sizes ever grow beyond the recalc constant.

🤖 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 `@framework/logstore/rdb.go` around lines 743 - 758, Update the transaction
flow around the loop over updates so large backfill batches do not hold locks
for all N individual updates at once. Chunk the updates using the existing
recalc batch-size constant, or use an equivalent CASE-based multi-row update,
while preserving both token_usage and cache_debug writes for every id and the
current error propagation behavior.
framework/logstore/billinghydrationgate_test.go (1)

24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

gets is appended without synchronization.

Only the hydration path calls Get in these tests, so today it is single-goroutine. If a future test exercises hybrid.FindByID/SearchLogs (which hydrate) concurrently with the upload workers, -race will flag this. A sync.Mutex plus a Len()/snapshot accessor would make the fake safe by construction.

🤖 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 `@framework/logstore/billinghydrationgate_test.go` around lines 24 - 32, Make
countingObjectStore.Get concurrency-safe by adding a sync.Mutex around accesses
to gets, and provide Len or snapshot accessor methods for synchronized
inspection. Preserve the existing delegation to InMemoryObjectStore.Get while
ensuring concurrent hydration and upload-worker activity is race-free.
🤖 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 `@framework/logstore/hybrid.go`:
- Around line 818-822: Update the final empty-token_usage guard in the
hydration/pricing flow around log.TokenUsage so it only returns the “hydrated
payload has no token_usage” error for rows billed on tokens. Allow speech, OCR,
and video_generation modality rows to proceed when their modality-specific usage
payload is present, preserving the existing error behavior for token-priced
rows.

---

Nitpick comments:
In `@framework/logstore/billinghydrationgate_test.go`:
- Around line 24-32: Make countingObjectStore.Get concurrency-safe by adding a
sync.Mutex around accesses to gets, and provide Len or snapshot accessor methods
for synchronized inspection. Preserve the existing delegation to
InMemoryObjectStore.Get while ensuring concurrent hydration and upload-worker
activity is race-free.

In `@framework/logstore/rdb.go`:
- Around line 743-758: Update the transaction flow around the loop over updates
so large backfill batches do not hold locks for all N individual updates at
once. Chunk the updates using the existing recalc batch-size constant, or use an
equivalent CASE-based multi-row update, while preserving both token_usage and
cache_debug writes for every id and the current error propagation behavior.

In `@plugins/logging/costrecalc_test.go`:
- Around line 57-62: Update TestRunCostRecalcJob_HydratesInBoundedChunks to
assert that the fakeRecalcStore backfilled collection remains empty after the
job runs. Use the existing backfilled field to enforce the documented invariant
that BulkBackfillBillingPayloads is never invoked when nothing is hydrated.
🪄 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 Plus

Run ID: df9fc73d-5020-4b9c-9bb3-0968e3a08e46

📥 Commits

Reviewing files that changed from the base of the PR and between e27f1d6 and ab0f06b.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go

Comment thread framework/logstore/hybrid.go Outdated
@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch 2 times, most recently from 89e564a to 4a10a92 Compare July 30, 2026 08:52

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 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 `@framework/logstore/payload.go`:
- Around line 124-126: Update the billing recomputation path so rows marked
ContentHidden && HasObject first attempt billing-only hydration from object
storage. Release the recovered payload after extracting billing inputs, and
classify the row as Unpriceable only when the object is unavailable or storage
is unconfigured; otherwise preserve normal pricing.

In `@plugins/logging/costrecalc.go`:
- Around line 17-26: The billing query currently materializes an entire 200-row
page, so payload-heavy modality data can exceed the intended memory bound before
chunk hydration. In plugins/logging/costrecalc.go lines 17-26, replace
costRecalcBatchSize with a payload-safe page size or split scalar paging from
per-chunk payload retrieval; apply the same bound to synchronous recalculation
in plugins/logging/operations.go lines 1342-1346. In
plugins/logging/operations.go lines 1494-1502, remove or revise any claim of a
flat memory bound unless DB-resident modality payloads are fetched per chunk.

In `@plugins/logging/operations.go`:
- Around line 410-415: Update the streamed OpenAI accumulation flow around
applyServedTierToEntry so the response envelope or accumulator carries the
served service_tier from the provider’s stream chunks. Pass that preserved tier
instead of nil when applying it to the usage entry, ensuring priority and flex
streams retain their metadata for repricing.
🪄 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 Plus

Run ID: cfdc3303-97f7-4df4-b5ea-f73b02c5d543

📥 Commits

Reviewing files that changed from the base of the PR and between ab0f06b and 89e564a.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go

Comment thread framework/logstore/payload.go Outdated
Comment thread plugins/logging/costrecalc.go Outdated
Comment thread plugins/logging/operations.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
framework/logstore/billinghydrationgate_test.go (1)

24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider guarding gets with a mutex.

Today only the test goroutine triggers Get, so this is safe — but it is one background hydration away from a -race failure. A sync.Mutex plus a Keys()-style accessor keeps the counter trustworthy if a future test exercises a concurrent read path.

🤖 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 `@framework/logstore/billinghydrationgate_test.go` around lines 24 - 32, Make
countingObjectStore.Get concurrency-safe by adding a sync.Mutex and protecting
accesses to gets, then provide a Keys()-style accessor that returns a safe
snapshot for assertions. Update tests to use the accessor rather than reading
gets directly, while preserving the existing object-store delegation.
🤖 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.

Nitpick comments:
In `@framework/logstore/billinghydrationgate_test.go`:
- Around line 24-32: Make countingObjectStore.Get concurrency-safe by adding a
sync.Mutex and protecting accesses to gets, then provide a Keys()-style accessor
that returns a safe snapshot for assertions. Update tests to use the accessor
rather than reading gets directly, while preserving the existing object-store
delegation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c3416523-5002-497f-b8ff-dcb96368ca9a

📥 Commits

Reviewing files that changed from the base of the PR and between 89e564a and 4a10a92.

📒 Files selected for processing (17)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • transports/bifrost-http/handlers/logging.go

@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch from 4a10a92 to 69305f7 Compare July 30, 2026 09:18

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
framework/streaming/accumulator.go (1)

49-60: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pooled ChatStreamChunk.LogProbs is never reset — stale logprobs can leak across requests.

putChatStreamChunk resets ServiceTier here but not LogProbs. In chat.go, chunk.LogProbs is only assigned when result.ChatResponse.Choices is non-empty with a delta choice; a usage-only/synthetic terminal final chunk (the exact scenario this PR's new tests exercise for ServiceTier) never touches LogProbs. Since getChatStreamChunk() does no reset on Get(), a pooled chunk reused for such a chunk retains whatever LogProbs a previous, unrelated request left behind, and the unconditional merge loop in processAccumulatedChatStreamingChunks (if chunk.LogProbs != nil { mergedLogProbs.Content = append(...) }) will splice that stale data into the current request's logged/returned response.

putResponsesStreamChunk similarly never resets ChunkIndex (same as putChatStreamChunk); worth a pass to confirm it can't be read stale before being overwritten (e.g. the error branch in responses.go feeds chunk.ChunkIndex into reserveTerminalChunkIndex before it's reassigned).

🐛 Proposed fix
 func (a *Accumulator) putChatStreamChunk(chunk *ChatStreamChunk) {
 	chunk.Timestamp = time.Time{}
 	chunk.Delta = nil
 	chunk.Cost = nil
 	chunk.SemanticCacheDebug = nil
 	chunk.ErrorDetails = nil
 	chunk.FinishReason = nil
 	chunk.TokenUsage = nil
 	chunk.ServiceTier = nil
+	chunk.LogProbs = nil
 	chunk.RawResponse = nil
 	a.chatStreamChunkPool.Put(chunk)
 }

As per coding guidelines, "Pooled objects must have every field reset before returning to a pool."

Also applies to: 104-115

🤖 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 `@framework/streaming/accumulator.go` around lines 49 - 60, Update
putChatStreamChunk to reset LogProbs before returning the chunk to the pool, and
update putResponsesStreamChunk to reset ChunkIndex as well. Verify the responses
error path cannot consume stale ChunkIndex before reassignment, particularly
where reserveTerminalChunkIndex uses it, preserving correct terminal indexing.

Source: Coding guidelines

🤖 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 `@framework/logstore/hybrid.go`:
- Around line 730-736: Remove the unconditional ContentHidden early return in
the hydration decision logic, allowing those rows to proceed through the
existing vouched/backfilled checks. Preserve fetching for legacy hidden rows
with blank pricing inputs while avoiding full-payload hydration for newly
written hidden rows whose TokenUsage and CacheDebug values are present; update
the affected test fixture to blank token_usage when it must represent a legacy
row.

---

Outside diff comments:
In `@framework/streaming/accumulator.go`:
- Around line 49-60: Update putChatStreamChunk to reset LogProbs before
returning the chunk to the pool, and update putResponsesStreamChunk to reset
ChunkIndex as well. Verify the responses error path cannot consume stale
ChunkIndex before reassignment, particularly where reserveTerminalChunkIndex
uses it, preserving correct terminal indexing.
🪄 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 Plus

Run ID: 7e79f0eb-d432-4891-963d-28dd49b217ca

📥 Commits

Reviewing files that changed from the base of the PR and between 4a10a92 and 69305f7.

📒 Files selected for processing (24)
  • framework/logstore/billinghydrationgate_test.go
  • framework/logstore/billingprojection_test.go
  • framework/logstore/contenthidden_test.go
  • framework/logstore/hybrid.go
  • framework/logstore/hybrid_test.go
  • framework/logstore/hybridbilling_test.go
  • framework/logstore/migrations.go
  • framework/logstore/payload.go
  • framework/logstore/rdb.go
  • framework/logstore/store.go
  • framework/logstore/tables.go
  • framework/streaming/accumulator.go
  • framework/streaming/accumulator_test.go
  • framework/streaming/chat.go
  • framework/streaming/responses.go
  • framework/streaming/responses_test.go
  • framework/streaming/types.go
  • plugins/logging/costfidelity_test.go
  • plugins/logging/costrecalc.go
  • plugins/logging/costrecalc_test.go
  • plugins/logging/main.go
  • plugins/logging/operations.go
  • plugins/logging/operations_test.go
  • transports/bifrost-http/handlers/logging.go

Comment thread framework/logstore/hybrid.go Outdated
@akshaydeo
akshaydeo force-pushed the 07-30-recalculate_cost_fixes branch from 69305f7 to 78d8e96 Compare July 30, 2026 09:40

akshaydeo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Jul 30, 9:40 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 30, 9:41 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit 341c226 into dev Jul 30, 2026
11 of 12 checks passed
@akshaydeo
akshaydeo deleted the 07-30-recalculate_cost_fixes branch July 30, 2026 09:41
This was referenced Jul 30, 2026
akshaydeo pushed a commit that referenced this pull request Aug 18, 2026
…r to final chunk and log entry (#6236)

## Summary

Anthropic reports `service_tier` on the `message_start` usage block during streaming. The per-event converter drops it, and `BifrostLLMUsage` has no `service_tier` field, so it had nowhere to travel. As a result, every streamed Anthropic request logged an empty `service_tier` and was repriced at standard rates instead of the actual served tier (priority/flex). This fix latches the tier across streaming events and stamps it onto the final chunk's response envelope, mirroring the existing pattern for `speed` and `inference_geo`.

## Changes

- In the Anthropic chat completion and responses streaming loops, `service_tier` from `message_start` usage is now latched into a `servedServiceTier` variable and applied to the final chunk's response envelope, matching how `speed` and `inference_geo` are already handled.
- `StreamAccumulatorResult` gains a `ServiceTier` field so the resolved tier survives the tracer boundary. Without this field, the tier was lost when the accumulator handed off to the tracer, causing streamed rows to reprice at standard rates.
- `ProcessStreamingChunk` in the tracer now copies `ServiceTier` from the processed response into the accumulator result explicitly, since it lives on the response envelope rather than inside `BifrostLLMUsage`.
- `convertToProcessedStreamResponse` in the logging plugin now forwards `ServiceTier` from `StreamAccumulatorResult` into the processed response so `applyStreamingOutputToEntry` can write it to the log entry.
- Tests added to verify the final chunk carries the correct `service_tier` for both the chat completion and responses streaming paths, and that the tier survives the full accumulator-to-log-entry handoff.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/anthropic/...
go test ./plugins/logging/...
go test ./...
```

The new test `TestAnthropicChatStreamFinalChunkCarriesServedServiceTier` replays a synthetic Anthropic SSE stream where `service_tier: priority` appears on `message_start` and asserts the final chunk's `ServiceTier` equals `priority` alongside the existing `speed` and `inference_geo` assertions.

`TestStreamingServiceTierSurvivesAccumulatorHandoff` verifies that a `StreamAccumulatorResult` carrying `priority` tier produces a log entry with `service_tier: priority` after the full conversion chain.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Related to the same class of mis-billing addressed in #5669 for non-streamed rows.

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akshaydeo pushed a commit that referenced this pull request Aug 19, 2026
…r to final chunk and log entry (#6236)

## Summary

Anthropic reports `service_tier` on the `message_start` usage block during streaming. The per-event converter drops it, and `BifrostLLMUsage` has no `service_tier` field, so it had nowhere to travel. As a result, every streamed Anthropic request logged an empty `service_tier` and was repriced at standard rates instead of the actual served tier (priority/flex). This fix latches the tier across streaming events and stamps it onto the final chunk's response envelope, mirroring the existing pattern for `speed` and `inference_geo`.

## Changes

- In the Anthropic chat completion and responses streaming loops, `service_tier` from `message_start` usage is now latched into a `servedServiceTier` variable and applied to the final chunk's response envelope, matching how `speed` and `inference_geo` are already handled.
- `StreamAccumulatorResult` gains a `ServiceTier` field so the resolved tier survives the tracer boundary. Without this field, the tier was lost when the accumulator handed off to the tracer, causing streamed rows to reprice at standard rates.
- `ProcessStreamingChunk` in the tracer now copies `ServiceTier` from the processed response into the accumulator result explicitly, since it lives on the response envelope rather than inside `BifrostLLMUsage`.
- `convertToProcessedStreamResponse` in the logging plugin now forwards `ServiceTier` from `StreamAccumulatorResult` into the processed response so `applyStreamingOutputToEntry` can write it to the log entry.
- Tests added to verify the final chunk carries the correct `service_tier` for both the chat completion and responses streaming paths, and that the tier survives the full accumulator-to-log-entry handoff.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [ ] Transports (HTTP)
- [x] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/anthropic/...
go test ./plugins/logging/...
go test ./...
```

The new test `TestAnthropicChatStreamFinalChunkCarriesServedServiceTier` replays a synthetic Anthropic SSE stream where `service_tier: priority` appears on `message_start` and asserts the final chunk's `ServiceTier` equals `priority` alongside the existing `speed` and `inference_geo` assertions.

`TestStreamingServiceTierSurvivesAccumulatorHandoff` verifies that a `StreamAccumulatorResult` carrying `priority` tier produces a log entry with `service_tier: priority` after the full conversion chain.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Related to the same class of mis-billing addressed in #5669 for non-streamed rows.

## Security considerations

None.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
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