Task #27: Apply 3 required fixes for cost-routing + free-tier gate

1. Add `estimateRequestCost(request, model)` to PricingService in pricing.ts
   - Unified method combining estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd
   - Replaces duplicated token estimation logic at call sites in jobs.ts, sessions.ts, estimate.ts

2. Move partial free-tier `recordGrant()` from invoice creation to post-work in runWorkInBackground
   - Previously called at invoice creation for partial path (before user pays) — economic DoS vulnerability
   - Now deferred to after work completes, using new `partialAbsorbSats` parameter in runWorkInBackground
   - Fully-free jobs still record grant at eval time (no payment involved)

3. Sessions pre-gate refactor: estimate → decide → execute → reconcile
   - Free-tier `decide()` now runs on ESTIMATED cost BEFORE `executeWork()` is called
   - After execution, `absorbedSats` is capped at actual cost (Math.min) to prevent over-absorption
   - Uses new `estimateRequestCost()` for clean single-call estimation
This commit is contained in:
alexpaynex
2026-03-19 16:43:41 +00:00
parent 4c3a0e867a
commit 512089ca08
4 changed files with 54 additions and 22 deletions

View File

@@ -159,6 +159,20 @@ export class PricingService {
return { amountSats, estimatedCostUsd, marginPct: this.marginPct, btcPriceUsd };
}
/**
* Combined estimate: input tokens + output tokens + work fee USD for a given request.
* Single call-site for pre-gate cost estimation — replaces duplicated logic in routes.
*/
estimateRequestCost(
requestText: string,
modelId: string,
): { estimatedInputTokens: number; estimatedOutputTokens: number; estimatedCostUsd: number } {
const estimatedInputTokens = this.estimateInputTokens(requestText);
const estimatedOutputTokens = this.estimateOutputTokens(requestText);
const estimatedCostUsd = this.calculateWorkFeeUsd(estimatedInputTokens, estimatedOutputTokens, modelId);
return { estimatedInputTokens, estimatedOutputTokens, estimatedCostUsd };
}
// ── Post-work honest accounting ──────────────────────────────────────────
/**