Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions cmd/gomodel/docs/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions docs/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions ext/ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ type Result struct {
// chain and never sent upstream; it must never contain secrets or
// request credentials.
Detail any
// TokensSaved is the rewriter's estimate of prompt tokens its body
// change removed from the request. When positive and the rewritten body
// is applied, core adds it to the request's usage record together with
// the input cost those tokens would have incurred, and the dashboard
// aggregates both as rewrite savings. Leave zero when the rewrite does
// not shrink the prompt.
TokensSaved int
}

// RequestRewriter rewrites raw JSON request bodies at ingress, after
Expand Down
27 changes: 26 additions & 1 deletion internal/admin/dashboard/static/js/modules/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
cache_write_input_tokens: 0,
total_input_cost: null,
total_output_cost: null,
total_cost: null
total_cost: null,
rewrite_tokens_saved: 0,
rewrite_cost_saved: null
};
},

Expand Down Expand Up @@ -519,6 +521,29 @@
return this.formatCost(summary.total_input_cost) + ' input + ' + this.formatCost(summary.total_output_cost) + ' output';
},

// --- Rewrite savings (request rewriters, e.g. token compression) ---
// Savings ride on provider usage rows, so the uncached summary holds
// the full totals. Cards only appear once a rewriter reported savings.
usagePageRewriteTokensSaved() {
const saved = Number((this.usageSummary && this.usageSummary.rewrite_tokens_saved) || 0);
return Number.isFinite(saved) && saved > 0 ? saved : 0;
},

usagePageRewriteSavingsVisible() {
return this.usagePageRewriteTokensSaved() > 0;
},

usagePageRewriteCostSaved() {
const summary = this.usageSummary || {};
return summary.rewrite_cost_saved === undefined ? null : summary.rewrite_cost_saved;
},

usagePageRewriteSavedTitle() {
const tokens = this.usagePageRewriteTokensSaved();
if (tokens <= 0) return '';
return this.formatNumber(tokens) + ' prompt tokens removed by request rewriters before reaching providers';
},

async fetchModelUsage() {
let controller = null;
try {
Expand Down
33 changes: 33 additions & 0 deletions internal/admin/dashboard/static/js/modules/usage.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,39 @@ test('usage page stat cards follow the log cache scope and derive hits from the
assert.equal(module.usagePageCostTitle(), '');
});

test('usage page rewrite savings cards show only when rewriters saved tokens', () => {
const module = createUsageModule();
module.formatNumber = (n) => String(n);

// No savings reported: cards hidden, zero-safe values.
module.usageSummary = { total_requests: 10 };
assert.equal(module.usagePageRewriteSavingsVisible(), false);
assert.equal(module.usagePageRewriteTokensSaved(), 0);
assert.equal(module.usagePageRewriteCostSaved(), null);
assert.equal(module.usagePageRewriteSavedTitle(), '');

// Savings with pricing: both cards visible with tokens and cost.
module.usageSummary = { rewrite_tokens_saved: 4200, rewrite_cost_saved: 0.0125 };
assert.equal(module.usagePageRewriteSavingsVisible(), true);
assert.equal(module.usagePageRewriteTokensSaved(), 4200);
assert.equal(module.usagePageRewriteCostSaved(), 0.0125);
assert.equal(
module.usagePageRewriteSavedTitle(),
'4200 prompt tokens removed by request rewriters before reaching providers'
);

// Savings without pricing: tokens card carries the value, cost stays null.
module.usageSummary = { rewrite_tokens_saved: 100, rewrite_cost_saved: null };
assert.equal(module.usagePageRewriteSavingsVisible(), true);
assert.equal(module.usagePageRewriteCostSaved(), null);

// Defensive: negative or garbage totals never surface.
module.usageSummary = { rewrite_tokens_saved: -5 };
assert.equal(module.usagePageRewriteSavingsVisible(), false);
module.usageSummary = { rewrite_tokens_saved: 'NaN?' };
assert.equal(module.usagePageRewriteSavingsVisible(), false);
});

test('fetchUsagePageSummary loads uncached and all cache modes with the page filters', async () => {
const { app, fetchCalls } = createUsageLogApp({ usageFilterLabel: 'env:prod' });

Expand Down
8 changes: 8 additions & 0 deletions internal/admin/dashboard/templates/page-usage.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ <h2>Usage Analytics</h2>
<div class="card-label">Estimated Cost</div>
<div class="card-value" x-text="formatCost(usageSummary.total_cost)" :title="usagePageCostTitle()">-</div>
</div>
<div class="card" x-show="usagePageRewriteSavingsVisible()">
<div class="card-label">Rewrite Saved</div>
<div class="card-value" x-text="formatCost(usagePageRewriteCostSaved())" :title="usagePageRewriteSavedTitle()">-</div>
</div>
<div class="card" x-show="usagePageRewriteSavingsVisible()">
<div class="card-label">Tokens Saved</div>
<div class="card-value" x-text="formatNumber(usagePageRewriteTokensSaved())" :title="usagePageRewriteSavedTitle()">-</div>
</div>
<div class="card" x-show="cacheAnalyticsEnabled()">
<div class="card-label">Cache Saved</div>
<div class="card-value" x-text="formatCost(cacheOverview.summary.total_saved_cost)">-</div>
Expand Down
26 changes: 26 additions & 0 deletions internal/core/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ const (
// requestOriginKey stores the logical request origin for internal execution
// flows that still reuse the translated request pipeline.
requestOriginKey contextKey = "request-origin"

// rewriteTokensSavedKey stores the total prompt tokens that applied
// request rewriters estimate they removed from the request body. Usage
// recording folds it into the request's usage entry as rewrite savings.
rewriteTokensSavedKey contextKey = "rewrite-tokens-saved"
)

// RequestOrigin identifies whether a request came from an external caller or an
Expand Down Expand Up @@ -226,6 +231,27 @@ func GetFailoverUsed(ctx context.Context) bool {
return false
}

// WithRewriteTokensSaved returns a new context carrying the total prompt
// tokens that applied request rewriters estimate they removed. Non-positive
// totals leave the context unchanged.
func WithRewriteTokensSaved(ctx context.Context, tokensSaved int) context.Context {
if tokensSaved <= 0 {
return ctx
}
return context.WithValue(ctx, rewriteTokensSavedKey, tokensSaved)
}

// RewriteTokensSavedFromContext retrieves the request's rewrite savings
// estimate, or zero when no rewriter reported savings.
func RewriteTokensSavedFromContext(ctx context.Context) int {
if v := ctx.Value(rewriteTokensSavedKey); v != nil {
if saved, ok := v.(int); ok && saved > 0 {
return saved
}
}
return 0
}

// WithRequestOrigin returns a new context with the logical request origin attached.
func WithRequestOrigin(ctx context.Context, origin RequestOrigin) context.Context {
return context.WithValue(ctx, requestOriginKey, origin)
Expand Down
1 change: 1 addition & 0 deletions internal/gateway/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (o *InferenceOrchestrator) logUsage(
entry.ProviderName = strings.TrimSpace(providerName)
entry.UserPath = core.UserPathFromContext(ctx)
entry.Labels = core.RequestLabelsFromContext(ctx)
usage.ApplyRewriteSavings(entry, core.RewriteTokensSavedFromContext(ctx), pricing)
o.usageLogger.Write(entry)
}
}
Expand Down
1 change: 1 addition & 0 deletions internal/server/passthrough_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func (s *passthroughService) proxyPassthroughResponse(c *echo.Context, providerT
if observer := usage.NewStreamUsageObserver(s.usageLogger, model, providerType, requestID, usagePath, s.pricingResolver, core.UserPathFromContext(c.Request().Context())); observer != nil {
observer.SetProviderName(providerName)
observer.SetLabels(core.RequestLabelsFromContext(c.Request().Context()))
observer.SetRewriteTokensSaved(core.RewriteTokensSavedFromContext(c.Request().Context()))
observers = append(observers, observer)
}
}
Expand Down
8 changes: 8 additions & 0 deletions internal/server/request_rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func RequestRewriteMiddleware(rewriters []ext.RequestRewriter, auditLogger audit
}

changed := false
tokensSaved := 0
for _, rw := range rewriters {
res, rwErr := rw.Rewrite(c.Request().Context(), in)
if rwErr != nil {
Expand All @@ -58,12 +59,19 @@ func RequestRewriteMiddleware(rewriters []ext.RequestRewriter, auditLogger audit
recordRequestRevision(c, auditLogger, rw.Name(), len(in.Body), res)
in.Body = res.Body
changed = true
if res.TokensSaved > 0 {
tokensSaved += res.TokensSaved
}
}
}

if changed {
pinOriginalAuditRequestBody(c, auditLogger)
applyRewrittenBody(c, in.Body)
if tokensSaved > 0 {
req := c.Request()
c.SetRequest(req.WithContext(core.WithRewriteTokensSaved(req.Context(), tokensSaved)))
}
}
return next(c)
}
Expand Down
Loading