package ai import ( "context" "fmt" "strings" "sync" "time" "github.com/google/uuid" "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" ) func newOpenAIClient(config models.AIConfig) openai.Client { 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)) } 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)), } } 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") } return item, nil }