feat: add configurable AgentRouter AI provider

This commit is contained in:
2026-08-22 21:47:41 +08:00
parent 4f91726edb
commit cf2f114dc8
20 changed files with 1204 additions and 79 deletions

View File

@@ -15,7 +15,7 @@ import (
var ErrDisabled = errors.New("growth report: disabled")
const MaxProviderRequests int64 = 4
const MaxProviderRequests int64 = maxAvalAIAttempts * maxAvalAIProviderTurns
type ServiceConfig struct {
Enabled bool
@@ -120,14 +120,18 @@ func (s *Service) fingerprint(merchantID int64, terminalID, watermark, catalogVe
type Processor struct {
repository *Repository
builder Builder
avalai *AvalAIClient
generator CopyGenerator
coordinator *Coordinator
aiEnabled bool
now func() time.Time
}
func NewProcessor(repository *Repository, builder Builder, avalai *AvalAIClient, coordinator *Coordinator, aiEnabled bool) *Processor {
return &Processor{repository: repository, builder: builder, avalai: avalai, coordinator: coordinator, aiEnabled: aiEnabled, now: time.Now}
type CopyGenerator interface {
GenerateWithTools(context.Context, json.RawMessage, KnownEvidence, GrowthAnalyticsTools) (AICopy, Usage, error)
}
func NewProcessor(repository *Repository, builder Builder, generator CopyGenerator, coordinator *Coordinator, aiEnabled bool) *Processor {
return &Processor{repository: repository, builder: builder, generator: generator, coordinator: coordinator, aiEnabled: aiEnabled, now: time.Now}
}
func (p *Processor) ProcessNext(ctx context.Context) error {
record, err := p.repository.Claim(ctx)
@@ -153,19 +157,19 @@ func (p *Processor) ProcessNext(ctx context.Context) error {
}
status := StatusPartial
failure := "AI_DISABLED_OR_UNAVAILABLE"
if analysis.ShouldCallAI && p.aiEnabled && p.avalai != nil {
if analysis.ShouldCallAI && p.aiEnabled && p.generator != nil {
reservedTokens := EstimatedTokenReservation(analysis.Encoded)
release, enterErr := p.coordinator.Enter(ctx)
if enterErr == nil {
defer release()
if err := p.coordinator.ReserveBudget(ctx, MaxProviderRequests, reservedTokens, p.now()); err == nil {
generated, usage, aiErr := p.avalai.GenerateWithTools(ctx, analysis.Encoded, analysis.KnownEvidence, NewEvidenceTools(analysis))
generated, usage, aiErr := p.generator.GenerateWithTools(ctx, analysis.Encoded, analysis.KnownEvidence, NewEvidenceTools(analysis))
usage.ReservedTokens = reservedTokens
if aiErr == nil {
slog.Info("growth AI generation complete", "report_id", record.ReportID, "provider_requests", usage.ProviderRequests, "reserved_tokens", usage.ReservedTokens, "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens, "latency_ms", usage.LatencyMS, "estimated_cost", usage.EstimatedCost, "provider_request_id", usage.ProviderRequestID)
return p.repository.CompleteClaimed(ctx, record.ReportID, record.ClaimToken, StatusReady, analysis.Encoded, &generated, usage, "")
}
slog.Warn("growth AI output rejected", "report_id", record.ReportID, "reason", "AI_REJECTED", "provider_requests", usage.ProviderRequests, "reserved_tokens", usage.ReservedTokens, "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens, "latency_ms", usage.LatencyMS, "estimated_cost", usage.EstimatedCost, "provider_request_id", usage.ProviderRequestID)
slog.Warn("growth AI output rejected", "report_id", record.ReportID, "reason", "AI_REJECTED", "error", aiErr, "provider_requests", usage.ProviderRequests, "reserved_tokens", usage.ReservedTokens, "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens, "latency_ms", usage.LatencyMS, "estimated_cost", usage.EstimatedCost, "provider_request_id", usage.ProviderRequestID)
failure = "AI_REJECTED"
return p.repository.CompleteClaimed(ctx, record.ReportID, record.ClaimToken, status, analysis.Encoded, analysis.Fallback, usage, failure)
} else {
@@ -190,12 +194,12 @@ func deterministicCompletion(hasRecommendations bool) (Status, string) {
return StatusReady, ""
}
// EstimatedTokenReservation reserves for two complete attempts, each with two
// provider turns, before any network call. Bytes are deliberately counted as
// tokens and the fixed allowance covers tool schemas, instructions, tool
// outputs, and every turn's 1,600-token output ceiling.
// EstimatedTokenReservation reserves for two complete attempts of the bounded
// tool workflow before any network call. Bytes are deliberately over-counted
// and the fixed allowance covers schemas, instructions, accumulated tool
// outputs, and every provider turn's configured output ceiling.
func EstimatedTokenReservation(evidence json.RawMessage) int64 {
return int64(12*len(evidence) + 32000)
return int64(30*len(evidence) + 64000 + int(MaxProviderRequests)*maxAvalAIOutputTokens)
}
func newReportID() string {