Skip to content

[NA] [BE] fix: recalculate the estimated cost on bulk span update - #8133

Open
aseem-ai wants to merge 1 commit into
comet-ml:mainfrom
aseem-ai:fix/bulk-span-update-recalculate-cost
Open

[NA] [BE] fix: recalculate the estimated cost on bulk span update#8133
aseem-ai wants to merge 1 commit into
comet-ml:mainfrom
aseem-ai:fix/bulk-span-update-recalculate-cost

Conversation

@aseem-ai

@aseem-ai aseem-ai commented Sep 3, 2026

Copy link
Copy Markdown

Details

PATCH /v1/private/spans/batch only wrote the cost columns when the client sent total_estimated_cost. The single-span update recomputes it through CostService when model, provider or usage change and no manual cost is given, so a span first written without a priceable model kept a zero or stale cost after a batch correction and under-reported project and experiment spend. This mirrors that branch into newBulkUpdateTemplate / bindBulkUpdateParams.

  • Bulk does not read the rows it updates, so the query itself keeps a manually set cost (total_estimated_cost > 0 AND total_estimated_cost_version = '') rather than going through isManualCost.
  • An explicit total_estimated_cost in the batch payload still wins and is still stored as manual (blank version).

Change checklist

  • User facing
  • Documentation update

Issues

AI-WATERMARK

AI-WATERMARK: [no]

  • If yes:
    • Tools:
    • Model(s):
    • Scope:
    • Human verification:

Testing

Run from apps/opik-backend on JDK 25, local Docker Testcontainers.

  • mvn -B test -Dtest=SpansBatchUpdateResourceTest -> 9 tests, 0 failures.
  • mvn -B test -Dtest="SpansResourceTest,SpansBatchUpdateResourceTest,SpanServiceImplTest,FindSpansResourceTest,SpansLocalV2TableTest" -> 1878 tests, 0 failures.
  • mvn spotless:check -> clean.

Scenarios, all in a new BatchUpdateCost nested class:

  • Batch update with a priceable model + usage and no cost -> cost recalculated and stored with the estimated version. Fails on main with Expecting actual not to be null.
  • Span with a manually set cost, same batch update -> manual cost kept, version stays blank.
  • Batch update with no priceable model -> stored cost untouched.

The existing batchUpdate__updateAllFields__success covers an explicit total_estimated_cost in the batch payload and still passes.

Not run: the full backend suite. CI splits it into 16 jobs; I ran every span test class instead. No screen recording.

Documentation

None needed. No API shape change.

Single span updates recompute total_estimated_cost through CostService when
model, provider or usage change and no manual cost is sent. The bulk path only
wrote the cost columns when the client sent total_estimated_cost, so spans
corrected through PATCH /v1/private/spans/batch kept a zero or stale cost and
under-reported project and experiment spend.

Bulk does not read the rows it updates, so the query itself keeps a cost the
user set manually, the way isManualCost does on the single span path.
@aseem-ai
aseem-ai requested a review from a team as a code owner September 3, 2026 16:49
@github-actions github-actions Bot added java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. 🟡 size/M labels Sep 3, 2026
Comment on lines +3223 to +3224
boolean shouldRecalculateEstimatedCost = spanUpdate.totalEstimatedCost() == null
&& isUpdateCostRecalculationAvailable(spanUpdate);

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.

Partial batch updates leave stale costs

The bulk template/binder forwards null fields from partial requests to CostService, so the recalculation branch is skipped and the old cost remains — should we resolve each span’s effective model, provider, and usage as update(...) does before calling it?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/main/java/com/comet/opik/domain/SpanDAO.java` around lines
3223-3224, fix `newBulkUpdateTemplate` and its bulk-update parameter flow so cost
recalculation does not evaluate a partial `SpanUpdate` directly. Before invoking the
template/binder, resolve each target span’s effective model, provider, and usage using
the same fallback logic as `update(...)` for single-span updates, then pass those
resolved values to `isUpdateCostRecalculationAvailable` and `CostService`. Preserve
explicit `totalEstimatedCost` values and ensure the resolved values are used
consistently for both template rendering and parameter binding.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That's the behavior on main too — a partial payload leaves the stored cost untouched, and this PR keeps that. A per-span merge doesn't fit the current bulk design: one statement applies one update to all ids, so a merge would need a per-span cost value. Single-span update only merges because it already loads the row for the manual-cost check. This covers the issue's case, a batch carrying model+usage together. I can open a follow-up issue for a per-span merge if that's wanted.

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.

Thanks for clarifying the bulk-update constraint and intended scope. I’ll save this to memory once the PR is merged.

Comment on lines +3229 to +3232
if (shouldRecalculateEstimatedCost) {
// Bulk doesn't load the rows it updates, so a cost the user set manually is kept by the
// query itself, the way isManualCost keeps it on the single span path.
template.add("recalculated_cost", "recalculated_cost");

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.

Multi-trace cost updates stay unaggregated

The bulk-update branch recalculates costs across spans with different persisted trace_ids, but SpanService builds SpansUpdated with only batchUpdate.update().traceId(), so finished-experiment aggregation skips the other traces and leaves denormalized aggregates stale — should we include all affected persisted trace IDs or span IDs in the event?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/main/java/com/comet/opik/domain/SpanDAO.java` around lines
3229-3232 and 3280-3284, fix the bulk cost-recalculation flow so it reports every span
actually updated, including spans whose persisted `trace_id` differs from the payload.
Refactor the bulk update DAO/service path to return or propagate all affected persisted
trace IDs (or span IDs), and update `SpanService` to construct `SpansUpdated` from that
complete set so experiment aggregations are refreshed for every changed trace.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That's pre-existing — main posts the same single-traceId event for every batch update, including ones with an explicit cost, and the bulk SQL never rewrites trace_id. This PR doesn't touch the event path. I can open a separate issue for propagating the persisted trace ids.

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.

Thanks for clarifying—the event-path issue is pre-existing and outside this PR's scope. I'll remember this context after the PR is merged.

Comment on lines +427 to +429
var updatedSpan = spanResourceClient.getById(id, TEST_WORKSPACE, API_KEY);
assertThat(updatedSpan.totalEstimatedCost())
.isEqualByComparingTo(CostService.calculateCost(MODEL, PROVIDER, USAGE, null));

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.

Production cost regressions are masked

The recalculated-cost test derives its expected value with CostService.calculateCost(...), the same production helper used by bindBulkUpdateParams, so a pricing regression can make both sides agree on the wrong value and still pass. Could we assert an independently derived literal cost for the fixed MODEL, PROVIDER, and USAGE fixture, or check the known price components directly?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansBatchUpdateResourceTest.java`
around lines 427-429, update
`batchUpdate__whenModelBecomesPriceable__thenRecalculateCost` so its expected cost does
not call `CostService.calculateCost`, which is also used by production code. Replace it
with the independently derived literal cost for the fixed `MODEL`, `PROVIDER`, and
`USAGE` fixture, or assert the known pricing components directly; remove the now-unused
`CostService` import if applicable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This follows the existing convention — the single-span cost tests in SpansResourceTest derive the expected value with CostService.calculateCost the same way. A hardcoded literal would break each time the price table updates. The version assertion independently checks the estimated-vs-manual distinction.

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.

Thanks for the context—using CostService.calculateCost here is intentional and consistent with the existing tests. I’ll save this to memory once the PR is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend java Pull requests that update Java code 🟡 size/M tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bulk span update does not recalculate total_estimated_cost when model/provider/usage change

1 participant