feat(tenant): bill AI points by output characters
Deployment Config CI / Deployment Config (push) Successful in 39s
Backend CI / Backend (push) Successful in 18m14s

Previously AI point cost was computed from request (input) characters at
reservation time. Switch to reserving a single point up front and
settling the final cost from the generated output length on completion.

- Reserve 1 point (or FixedPoints) instead of pricing on request chars
- On completion, recompute points from output chars vs base chars,
  update the reservation amounts, and post a ledger delta for the
  top-up/refund; invalidate the workspace quota summary cache
- MarkCompleted now persists final request_chars/base_chars/points
- Use ceil division in calculateAIPointCost so an exact base boundary
  charges one point instead of rolling over
- Thread output text through all CompleteAIPoints callers (article
  selection, KOL assist, question expansion, template assist, compliance
  judge) and return the settled reservation
- Add unit tests plus an integration test gated on TEST_DATABASE_URL
- Update the user manual: billing is by output characters

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 17:04:37 +08:00
parent c8dceb43d7
commit f69edc2218
12 changed files with 612 additions and 51 deletions
@@ -135,7 +135,7 @@ func (s *ArticleSelectionOptimizeService) OptimizeSelection(
}
completeCtx, cancel := newGenerationCleanupContext()
if err := CompleteAIPoints(completeCtx, s.pool, actor.TenantID, *reservation, result.Model); err != nil {
if _, err := CompleteAIPoints(completeCtx, s.pool, s.cache, actor.TenantID, *reservation, result.Model, content); err != nil {
cancel()
return nil, response.ErrInternal(50019, "article_selection_optimize_failed", "failed to confirm ai points usage")
}