Skip to content

feat(usage): track prompt tokens and cost saved by request rewriters#481

Merged
SantiagoDePolonia merged 2 commits into
mainfrom
feat/rewrite-savings
Jul 5, 2026
Merged

feat(usage): track prompt tokens and cost saved by request rewriters#481
SantiagoDePolonia merged 2 commits into
mainfrom
feat/rewrite-savings

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What

Request rewriters registered through the ext API can now report how many prompt tokens their rewrite removed (ext.Result.TokensSaved), and the gateway turns that into dashboard-visible savings: total tokens saved and the estimated money saved, aggregated in usage tracking.

New cards on the Usage page (shown only once a rewriter reports savings — zero visual change otherwise):

Card Value
Rewrite Saved estimated input cost avoided ($), same period/filters as Estimated Cost
Tokens Saved prompt tokens removed before reaching providers

How

  • ext API: Result.TokensSaved int — a rewriter's estimate of prompt tokens its applied body change removed.
  • Middleware: RequestRewriteMiddleware sums the estimates of rewriters whose body was applied and stores the total on the request context (core.WithRewriteTokensSaved).
  • Usage recording: both write paths stamp entries — the orchestrator's non-streaming logUsage and StreamUsageObserver (SetRewriteTokensSaved, wired at both construction sites). Local response-cache hits never carry savings (no provider call was made).
  • Cost math: usage.ApplyRewriteSavings prices removed tokens as the input-cost delta between the request as the client sent it (input + saved tokens) and as forwarded, via the existing CalculateGranularCost — so endpoint-adjusted (batch) rates and tiered pricing stay honest: when the un-rewritten prompt would have landed in a higher tier, the delta includes re-rating the whole input. Unknown pricing → cost stays nil, tokens still counted.
  • Storage: rewrite_tokens_saved / rewrite_cost_saved columns in SQLite and PostgreSQL (idempotent migrations, fresh-DB DDL); MongoDB persists via bson tags. GetSummary sums them on all three backends with the standard filters.
  • Pricing recalculation (/admin/usage/recalculate-pricing) refreshes rewrite_cost_saved from the stored token estimate alongside the other cost fields on all three backends.

Tests

  • ApplyRewriteSavings unit table: flat rate, nil pricing, tier crossing, batch endpoint.
  • SQLite store→summary roundtrip incl. null-cost semantics (unpriced savings keep cost null, not 0).
  • SQLite pricing recalculation refreshes a stale rewrite_cost_saved.
  • Middleware: sums across applied rewriters into context; body change without a savings claim leaves context zero.
  • Stream observer: savings + pricing → cost on the extracted entry; no pricing → tokens only.
  • Dashboard cjs: card visibility/formatting incl. defensive inputs (403 dashboard tests pass).
  • Full suite: go test ./..., make test-dashboard, make test-e2e, make lint, make build — all green. Swagger/openapi regenerated.

Notes for reviewers

  • Savings are estimates (rewriters estimate tokens; cost uses static model pricing). Provider-reported exact costs (OpenRouter credits, xAI ticks) are not used for the delta since they cannot be recomputed hypothetically.
  • First consumer is GoModel Pro's token-compression rewriter (separate repo/PR), but the mechanism is neutral — any rewriter that shrinks prompts can report savings.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added request-rewrite revision telemetry to audit logs and the OpenAPI schema.
    • Usage analytics now tracks rewrite savings, including prompt tokens saved and estimated cost saved (including streaming responses).
    • The usage dashboard now displays “Rewrite savings” cards when savings are available.
  • Bug Fixes
    • Empty or error usage views now consistently initialize rewrite-savings fields to prevent missing/undefined values.

Request rewriters (ext extensions) can now report how many prompt tokens
their body rewrite removed via ext.Result.TokensSaved. Core aggregates
this into usage tracking and the dashboard:

- The rewrite middleware sums applied rewriters' estimates and carries
  the total on the request context.
- Both usage write paths fold it into the request's usage entry: the
  orchestrator's non-streaming logUsage and the SSE stream observer
  (wired at both construction sites).
- usage.ApplyRewriteSavings prices the removed tokens as the input-cost
  delta between the request as sent and as forwarded, via the existing
  granular cost engine — so tiered, batch, and endpoint-adjusted rates
  stay honest (a tier crossing re-rates the whole input).
- New usage columns rewrite_tokens_saved / rewrite_cost_saved in the
  SQLite and PostgreSQL stores (idempotent migrations; MongoDB persists
  via bson tags), summed into GetSummary on all three backends.
- Pricing recalculation refreshes rewrite_cost_saved from the stored
  token estimate alongside the other cost fields.
- Usage page gains "Rewrite Saved" (cost) and "Tokens Saved" cards next
  to Cache Saved, shown only once a rewriter reports savings — zero
  visual change for gateways without rewriters.

Savings ride on provider usage rows only; local response-cache hits
never carry them (no provider call, nothing saved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 5, 2026, 8:53 AM

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

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c8d9a1d2-eff1-4ccc-b838-13d7b866c0da

📥 Commits

Reviewing files that changed from the base of the PR and between c1ae46d and 4cf0bba.

📒 Files selected for processing (2)
  • internal/server/request_rewrite_test.go
  • tests/integration/admin_test.go

📝 Walkthrough

Walkthrough

This PR adds rewrite token and cost savings tracking from request rewriting through usage storage, aggregation, recalculation, dashboard display, and API schemas.

Changes

Rewrite Savings Tracking

Layer / File(s) Summary
Rewriter contract and context propagation
ext/ext.go, internal/server/request_rewrite.go, internal/server/request_rewrite_test.go, internal/core/context.go, internal/server/passthrough_support.go, internal/server/translated_inference_service.go
Result gains TokensSaved; request rewriting accumulates and stores it in context, with tests covering applied-body and ignored-savings cases and stream observers receiving the context value.
Savings computation and usage entry wiring
internal/usage/savings.go, internal/usage/usage.go, internal/usage/reader.go, internal/usage/stream_observer.go, internal/usage/stream_observer_test.go, internal/gateway/usage.go, internal/admin/dashboard/static/js/modules/usage.js, internal/admin/dashboard/static/js/modules/usage.test.cjs
Rewrite savings helpers compute token and cost savings, usage entry and summary shapes gain new fields, and logging/streaming/dashboard code applies and displays the values.
Persistence: table schema and batch inserts
internal/usage/store_postgresql.go, internal/usage/store_postgresql_test.go, internal/usage/store_sqlite.go
Usage table schemas, migrations, and batch insert builders are extended to persist rewrite savings fields.
Summary aggregation and pricing recalculation
internal/usage/reader_mongodb.go, internal/usage/reader_postgresql.go, internal/usage/reader_sqlite.go, internal/usage/recalculate_pricing.go, internal/usage/recalculate_pricing_mongodb.go, internal/usage/recalculate_pricing_postgresql.go, internal/usage/recalculate_pricing_sqlite.go, internal/usage/savings_test.go, tests/integration/admin_test.go
Summary queries and pricing recalculation paths read and recompute rewrite savings across backends, with unit and integration coverage for aggregation and repricing.
API schema updates
cmd/gomodel/docs/docs.go, docs/openapi.json
Generated OpenAPI definitions add request revision snapshots and usage rewrite savings fields.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Rewriter
  participant RequestRewriteMiddleware
  participant RequestContext
  participant StreamUsageObserver
  participant UsageStore

  Rewriter->>RequestRewriteMiddleware: Result{TokensSaved}
  RequestRewriteMiddleware->>RequestContext: WithRewriteTokensSaved(tokensSaved)
  RequestContext->>StreamUsageObserver: RewriteTokensSavedFromContext
  StreamUsageObserver->>StreamUsageObserver: ApplyRewriteSavings(entry, tokensSaved, pricing)
  StreamUsageObserver->>UsageStore: UsageEntry{RewriteTokensSaved, RewriteCostSaved}
Loading

Possibly related PRs

  • ENTERPILOT/GoModel#158: Shares the same StreamUsageObserver plumbing used here to carry rewrite-savings data into usage entries.
  • ENTERPILOT/GoModel#289: Uses the same pricing recalculation paths extended here to include rewrite_cost_saved and rewrite_tokens_saved.
  • ENTERPILOT/GoModel#428: Modifies the same usage dashboard initialization and rendering flow extended here with new summary fields.

Poem

A bunny saw the prompts go lean,
With tokens saved and costs made clean.
Through hops of code, the numbers flowed,
And usage cards their savings ցույց?

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: tracking prompt tokens and cost saved by request rewriters.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 feat/rewrite-savings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov-commenter

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge with low risk.

Rewrite savings are consistently wired through middleware, usage recording, storage, summaries, recalculation, API docs, and dashboard display. No accepted runtime, data, or security issues were identified.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Validated the rewrite visibility contract by capturing the before/zero-savings state and the after/positive-savings state.
  • Verified that the targeted usage module tests passed, including the rewrite savings visibility unit case.
  • Reviewed the zero-savings dashboard screenshot to confirm rewrite cards are absent in that state.
  • Reviewed the positive-savings dashboard screenshot to confirm the formatted rewrite savings cards appear.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant RewriteMW as RequestRewriteMiddleware
participant Context as Request Context
participant Provider
participant Usage as Usage Recorder
participant Store as Usage Store
participant Dashboard

Client->>RewriteMW: POST inference request
RewriteMW->>RewriteMW: Run registered ext.RequestRewriter chain
RewriteMW->>Context: Store summed TokensSaved when body rewrites apply
RewriteMW->>Provider: Forward rewritten request body
Provider-->>Usage: Return usage tokens in response/stream
Usage->>Context: Read rewrite tokens saved
Usage->>Usage: ApplyRewriteSavings with model pricing
Usage->>Store: Persist rewrite_tokens_saved and rewrite_cost_saved
Dashboard->>Store: GetSummary with active filters
Store-->>Dashboard: Aggregate tokens and nullable saved cost
Dashboard->>Dashboard: "Show Rewrite Saved / Tokens Saved cards when tokens > 0"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant RewriteMW as RequestRewriteMiddleware
participant Context as Request Context
participant Provider
participant Usage as Usage Recorder
participant Store as Usage Store
participant Dashboard

Client->>RewriteMW: POST inference request
RewriteMW->>RewriteMW: Run registered ext.RequestRewriter chain
RewriteMW->>Context: Store summed TokensSaved when body rewrites apply
RewriteMW->>Provider: Forward rewritten request body
Provider-->>Usage: Return usage tokens in response/stream
Usage->>Context: Read rewrite tokens saved
Usage->>Usage: ApplyRewriteSavings with model pricing
Usage->>Store: Persist rewrite_tokens_saved and rewrite_cost_saved
Dashboard->>Store: GetSummary with active filters
Store-->>Dashboard: Aggregate tokens and nullable saved cost
Dashboard->>Dashboard: "Show Rewrite Saved / Tokens Saved cards when tokens > 0"
Loading

Reviews (1): Last reviewed commit: "feat(usage): track prompt tokens and cos..." | Re-trigger Greptile

@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

Caution

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

⚠️ Outside diff range comments (1)
internal/usage/recalculate_pricing_postgresql.go (1)

68-94: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add PostgreSQL coverage for rewrite savings recalculation. The rewrite_tokens_saved path is only exercised by the SQLite recalculation test; add an equivalent PostgreSQL test that seeds rewrite_tokens_saved, runs RecalculatePricing, and asserts rewrite_cost_saved is refreshed.

🤖 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/usage/recalculate_pricing_postgresql.go` around lines 68 - 94, Add
PostgreSQL test coverage for the rewrite savings recalculation path in
RecalculatePricing. Create an equivalent PostgreSQL test that seeds usage rows
with rewrite_tokens_saved, runs the recalculation flow, and verifies
rewrite_cost_saved is updated/refreshed. Use the recalculation logic around
recalculationEntry and the postgres usage query/scan path in
internal/usage/recalculate_pricing_postgresql.go as the target behavior to
exercise.

Source: Path instructions

🤖 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 7833-7839: Add the nullable marker to the source field for
RewriteCostSaved in internal/usage/usage.go, since it is a pointer-backed float
like the other cost fields but currently generates a non-nullable schema. Update
the struct tag on RewriteCostSaved to include extensions:"x-nullable", then
regenerate the docs so the schema for rewrite_cost_saved reflects nullability
consistently with the related usage fields.

In `@internal/server/request_rewrite_test.go`:
- Around line 439-496: Add a regression test in RequestRewriteMiddleware for a
rewriter that returns TokensSaved > 0 but leaves Result.Body nil, since the
middleware should not accumulate savings unless a body rewrite is actually
applied. Use the existing test patterns in
TestRequestRewriteMiddlewareStoresTokensSavedInContext and
TestRequestRewriteMiddlewareNoSavingsLeavesContextZero, and assert
RewriteTokensSavedFromContext stays 0 when a stubRewriter reports savings
without changing the body.

---

Outside diff comments:
In `@internal/usage/recalculate_pricing_postgresql.go`:
- Around line 68-94: Add PostgreSQL test coverage for the rewrite savings
recalculation path in RecalculatePricing. Create an equivalent PostgreSQL test
that seeds usage rows with rewrite_tokens_saved, runs the recalculation flow,
and verifies rewrite_cost_saved is updated/refreshed. Use the recalculation
logic around recalculationEntry and the postgres usage query/scan path in
internal/usage/recalculate_pricing_postgresql.go as the target behavior to
exercise.
🪄 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: 8e35f419-ab9e-4135-9a4e-863db7fac1f3

📥 Commits

Reviewing files that changed from the base of the PR and between 9e21e80 and c1ae46d.

📒 Files selected for processing (28)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • ext/ext.go
  • internal/admin/dashboard/static/js/modules/usage.js
  • internal/admin/dashboard/static/js/modules/usage.test.cjs
  • internal/admin/dashboard/templates/page-usage.html
  • internal/core/context.go
  • internal/gateway/usage.go
  • internal/server/passthrough_support.go
  • internal/server/request_rewrite.go
  • internal/server/request_rewrite_test.go
  • internal/server/translated_inference_service.go
  • internal/usage/reader.go
  • internal/usage/reader_mongodb.go
  • internal/usage/reader_postgresql.go
  • internal/usage/reader_sqlite.go
  • internal/usage/recalculate_pricing.go
  • internal/usage/recalculate_pricing_mongodb.go
  • internal/usage/recalculate_pricing_postgresql.go
  • internal/usage/recalculate_pricing_sqlite.go
  • internal/usage/savings.go
  • internal/usage/savings_test.go
  • internal/usage/store_postgresql.go
  • internal/usage/store_postgresql_test.go
  • internal/usage/store_sqlite.go
  • internal/usage/stream_observer.go
  • internal/usage/stream_observer_test.go
  • internal/usage/usage.go

Comment thread cmd/gomodel/docs/docs.go
Comment on lines +7833 to +7839
"rewrite_cost_saved": {
"type": "number"
},
"rewrite_tokens_saved": {
"description": "Rewrite savings: prompt tokens request rewriters removed before the\nprovider call, and the estimated input cost avoided (nil when no\nmatched row had a priced savings estimate).",
"type": "integer"
},

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm whether RewriteCostSaved lacks the x-nullable extension tag, and compare with a sibling pointer field that already has it.
rg -n -B2 -A2 'RewriteCostSaved' internal/usage/usage.go
rg -n -B2 -A2 'InputCost \*float64' internal/usage/usage.go

Repository: ENTERPILOT/GoModel

Length of output: 558


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## usage.go\n'
rg -n -B3 -A3 'RewriteCostSaved|InputCost \*float64' internal/usage/usage.go || true

printf '\n## docs.go rewrite_cost_saved context\n'
rg -n -B4 -A4 '"rewrite_cost_saved"|x-nullable' cmd/gomodel/docs/docs.go | sed -n '1,120p'

printf '\n## other pointer-backed cost fields in docs.go\n'
rg -n -B2 -A4 '"input_cost"|x-nullable' cmd/gomodel/docs/docs.go | sed -n '1,160p'

Repository: ENTERPILOT/GoModel

Length of output: 7850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact schema block around rewrite_cost_saved.
sed -n '7826,7845p' cmd/gomodel/docs/docs.go

# Show the struct definition with surrounding comments in internal/usage/usage.go.
sed -n '80,98p' internal/usage/usage.go

Repository: ENTERPILOT/GoModel

Length of output: 1975


Mark RewriteCostSaved as nullable in the source struct.
internal/usage/usage.go defines it as *float64 but omits extensions:"x-nullable", so the generated schema shows "type": "number" without nullability while sibling pointer-backed cost fields include it. Add the tag there and regenerate docs.

🤖 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 `@cmd/gomodel/docs/docs.go` around lines 7833 - 7839, Add the nullable marker
to the source field for RewriteCostSaved in internal/usage/usage.go, since it is
a pointer-backed float like the other cost fields but currently generates a
non-nullable schema. Update the struct tag on RewriteCostSaved to include
extensions:"x-nullable", then regenerate the docs so the schema for
rewrite_cost_saved reflects nullability consistently with the related usage
fields.

Comment thread internal/server/request_rewrite_test.go
…ecalculation

Review follow-ups on #481:
- Regression tests that a rewriter claiming TokensSaved without an
  applied body rewrite contributes nothing — alone (context stays zero)
  and mid-chain (excluded from the accumulated sum).
- PostgreSQL integration test exercising the savings recalculation path
  end to end: a usage row with rewrite_tokens_saved and a stale
  rewrite_cost_saved is repriced to the exact input-cost delta by
  POST /admin/usage/recalculate-pricing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SantiagoDePolonia SantiagoDePolonia merged commit 1085323 into main Jul 5, 2026
20 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