Files

133 lines
4.3 KiB
Go
Raw Permalink Normal View History

2026-04-09 10:01:23 +08:00
package ai
import (
"context"
"fmt"
"strings"
"sync"
2026-04-09 10:01:23 +08:00
"time"
"github.com/google/uuid"
2026-04-09 10:01:23 +08:00
"github.com/mlogclub/simple/sqls"
openai "github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/tracex"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
2026-04-09 10:01:23 +08:00
)
func newOpenAIClient(config models.AIConfig) openai.Client {
2026-04-09 10:01:23 +08:00
opts := []option.RequestOption{
option.WithAPIKey(config.APIKey),
option.WithBaseURL(config.BaseURL),
}
if config.TimeoutMS > 0 {
opts = append(opts, option.WithRequestTimeout(time.Duration(config.TimeoutMS)*time.Millisecond))
}
if config.HTTPClient != nil {
opts = append(opts, option.WithHTTPClient(config.HTTPClient))
}
2026-04-09 10:01:23 +08:00
if config.MaxRetryCount >= 0 {
opts = append(opts, option.WithMaxRetries(config.MaxRetryCount))
}
return openai.NewClient(opts...)
}
type platformAIRequestScopeContextKey struct{}
type platformAIRequestPurposeContextKey struct{}
type platformAIRequestScope struct {
base string
mu sync.Mutex
next map[string]uint64
}
// WithPlatformAIRequestScope binds a persisted business operation identity to
// platform AI calls. Recreating a scope with the same base during recovery
// reproduces the same purpose/ordinal request IDs, while one live scope gives
// every logical upstream call a distinct ordinal.
func WithPlatformAIRequestScope(ctx context.Context, base string) context.Context {
if ctx == nil {
ctx = context.Background()
}
base = strings.TrimSpace(base)
if base == "" {
return ctx
}
return context.WithValue(ctx, platformAIRequestScopeContextKey{}, &platformAIRequestScope{
base: base,
next: make(map[string]uint64),
})
}
// WithPlatformAIRequestPurpose separates otherwise identical calls belonging
// to different stages such as retrieval and document indexing.
func WithPlatformAIRequestPurpose(ctx context.Context, purpose string) context.Context {
if ctx == nil {
ctx = context.Background()
}
purpose = strings.TrimSpace(purpose)
if purpose == "" {
return ctx
}
return context.WithValue(ctx, platformAIRequestPurposeContextKey{}, purpose)
}
func ensurePlatformAIRequestScope(ctx context.Context) context.Context {
if ctx == nil {
ctx = context.Background()
}
if scope, _ := ctx.Value(platformAIRequestScopeContextKey{}).(*platformAIRequestScope); scope != nil && strings.TrimSpace(scope.base) != "" {
return ctx
}
if requestID := tracex.RequestIDFromContext(ctx); requestID != "" {
return WithPlatformAIRequestScope(ctx, "request:"+requestID)
}
// No durable business identity is available (for example a one-off debug
// call), so create one scope for the public operation. Callers with recovery
// semantics must bind their persisted identity explicitly.
return WithPlatformAIRequestScope(ctx, "operation:"+uuid.NewString())
}
func nextPlatformAIRequestID(ctx context.Context, defaultPurpose string) string {
ctx = ensurePlatformAIRequestScope(ctx)
scope, _ := ctx.Value(platformAIRequestScopeContextKey{}).(*platformAIRequestScope)
purpose, _ := ctx.Value(platformAIRequestPurposeContextKey{}).(string)
purpose = strings.TrimSpace(purpose)
if purpose == "" {
purpose = strings.TrimSpace(defaultPurpose)
}
if purpose == "" {
purpose = "request"
}
scope.mu.Lock()
scope.next[purpose]++
ordinal := scope.next[purpose]
scope.mu.Unlock()
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(fmt.Sprintf("%s:purpose:%s:call:%d", scope.base, purpose, ordinal))).String()
}
// platformRequestOptions creates one deterministic idempotency key for one
// logical upstream call. SDK retries reuse these options. A later call in the
// same operation gets the next ordinal; recovery recreates the same sequence.
func platformRequestOptions(ctx context.Context, config models.AIConfig, purpose string) []option.RequestOption {
if !config.Platform {
return nil
}
return []option.RequestOption{
option.WithHeader("X-AI-Request-ID", nextPlatformAIRequestID(ctx, purpose)),
}
}
2026-04-09 10:01:23 +08:00
func GetEnabledAIConfig(modelType enums.AIModelType) (*models.AIConfig, error) {
item := repositories.AIConfigRepository.GetEnabled(sqls.DB(), modelType)
if item == nil {
return nil, errorsx.BusinessErrorI18n(2005, "error.aiConfig.noneEnabled")
2026-04-09 10:01:23 +08:00
}
return item, nil
}