feat(server): meter AI point usage across tenant AI features
Adds an ai_point_usage_logs ledger and a reserve/refund/complete pipeline that charges AI points for article selection optimize, template analyze /title/outline, and KOL prompt generate/optimize. Pending reservations are reconciled when the kol-assist worker and template-assist tasks finish or fail, so points refund automatically on errors. Workspace now exposes the AI quota status and a paginated usage ledger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,11 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/llm"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
||||
tenantapp "github.com/geo-platform/tenant-api/internal/tenant/app"
|
||||
@@ -20,10 +22,12 @@ import (
|
||||
var errKolAssistConsumerClosed = errors.New("kol assist consumer channel closed")
|
||||
|
||||
type KolAssistWorker struct {
|
||||
pool *pgxpool.Pool
|
||||
rabbitMQ *rabbitmq.Client
|
||||
repo repository.KolAssistRepository
|
||||
llm llm.Client
|
||||
logger *zap.Logger
|
||||
cache sharedcache.Cache
|
||||
retryInterval time.Duration
|
||||
processTimeout time.Duration
|
||||
consumerPrefix string
|
||||
@@ -31,10 +35,12 @@ type KolAssistWorker struct {
|
||||
}
|
||||
|
||||
func NewKolAssistWorker(
|
||||
pool *pgxpool.Pool,
|
||||
rabbitMQClient *rabbitmq.Client,
|
||||
repo repository.KolAssistRepository,
|
||||
llmClient llm.Client,
|
||||
logger *zap.Logger,
|
||||
cache sharedcache.Cache,
|
||||
workerConcurrency int,
|
||||
processTimeout time.Duration,
|
||||
) *KolAssistWorker {
|
||||
@@ -46,10 +52,12 @@ func NewKolAssistWorker(
|
||||
}
|
||||
|
||||
return &KolAssistWorker{
|
||||
pool: pool,
|
||||
rabbitMQ: rabbitMQClient,
|
||||
repo: repo,
|
||||
llm: llmClient,
|
||||
logger: logger,
|
||||
cache: cache,
|
||||
retryInterval: 5 * time.Second,
|
||||
processTimeout: processTimeout,
|
||||
consumerPrefix: fmt.Sprintf("kol-assist-%d", os.Getpid()),
|
||||
@@ -73,7 +81,19 @@ func (w *KolAssistWorker) Process(ctx context.Context, job tenantapp.KolAssistJo
|
||||
if err != nil {
|
||||
return fmt.Errorf("load kol assist task: %w", err)
|
||||
}
|
||||
if task == nil || task.Status == "completed" || task.Status == "failed" {
|
||||
if task == nil {
|
||||
return nil
|
||||
}
|
||||
switch task.Status {
|
||||
case "completed":
|
||||
w.completeAIPoints(ctx, job, "")
|
||||
return nil
|
||||
case "failed":
|
||||
errorMessage := "kol assist task failed"
|
||||
if task.ErrorMessage != nil && *task.ErrorMessage != "" {
|
||||
errorMessage = *task.ErrorMessage
|
||||
}
|
||||
w.refundAIPoints(ctx, job, errorMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -83,6 +103,7 @@ func (w *KolAssistWorker) Process(ctx context.Context, job tenantapp.KolAssistJo
|
||||
|
||||
if err := w.processTask(ctx, job, task); err != nil {
|
||||
_ = w.repo.MarkFailed(context.Background(), job.TenantID, job.TaskID, err.Error())
|
||||
w.refundAIPoints(context.Background(), job, err.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.Warn("kol assist task failed",
|
||||
zap.Error(err),
|
||||
@@ -215,5 +236,30 @@ func (w *KolAssistWorker) processTask(ctx context.Context, job tenantapp.KolAssi
|
||||
if err := w.repo.MarkCompleted(ctx, job.TenantID, job.TaskID, resultJSON); err != nil {
|
||||
return fmt.Errorf("mark kol assist completed: %w", err)
|
||||
}
|
||||
w.completeAIPoints(ctx, job, result.Model)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *KolAssistWorker) completeAIPoints(ctx context.Context, job tenantapp.KolAssistJob, model string) {
|
||||
resourceType := "kol_assist_task"
|
||||
resourceUID := job.TaskID
|
||||
if err := tenantapp.CompletePendingAIPointsByResource(ctx, w.pool, job.TenantID, resourceType, nil, &resourceUID, model); err != nil && w.logger != nil {
|
||||
w.logger.Warn("kol assist ai points confirmation failed",
|
||||
zap.Error(err),
|
||||
zap.String("task_id", job.TaskID),
|
||||
zap.Int64("tenant_id", job.TenantID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *KolAssistWorker) refundAIPoints(ctx context.Context, job tenantapp.KolAssistJob, errorMessage string) {
|
||||
resourceType := "kol_assist_task"
|
||||
resourceUID := job.TaskID
|
||||
if err := tenantapp.RefundPendingAIPointsByResource(ctx, w.pool, w.cache, job.TenantID, resourceType, nil, &resourceUID, errorMessage); err != nil && w.logger != nil {
|
||||
w.logger.Warn("kol assist ai points refund failed",
|
||||
zap.Error(err),
|
||||
zap.String("task_id", job.TaskID),
|
||||
zap.Int64("tenant_id", job.TenantID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user