feat(tenant): bill AI points by output characters
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:
@@ -1,6 +1,7 @@
|
||||
package compliance
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sharedcompliance "github.com/geo-platform/tenant-api/internal/shared/compliance"
|
||||
@@ -34,3 +35,39 @@ func TestDecidePassesWithoutHits(t *testing.T) {
|
||||
t.Fatalf("decide without hits = %s, want %s", got, sharedcompliance.GateDecisionPass)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplianceAIPointCostUsesOutputBoundary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
chars int
|
||||
baseChars int
|
||||
want int
|
||||
}{
|
||||
{name: "empty output minimum", chars: 0, baseChars: 1000, want: 1},
|
||||
{name: "single char", chars: 1, baseChars: 1000, want: 1},
|
||||
{name: "exactly one base", chars: 1000, baseChars: 1000, want: 1},
|
||||
{name: "one over base", chars: 1001, baseChars: 1000, want: 2},
|
||||
{name: "default base", chars: 2000, baseChars: 0, want: 2},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := calculateAIPointCost(tt.chars, tt.baseChars); got != tt.want {
|
||||
t.Fatalf("calculateAIPointCost(%d, %d) = %d, want %d", tt.chars, tt.baseChars, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplianceAIPointCharsTrimWhitespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := countAIPointChars(" " + strings.Repeat("字", 1001) + "\n")
|
||||
if got != 1001 {
|
||||
t.Fatalf("countAIPointChars = %d, want 1001", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user