package promptkit import ( "context" "errors" "fmt" "strings" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/jsonvalue" "gitea.maximumdirect.net/eric/promptkit/internal/profile" ) // OpenAICompatibleProfile returns an ordinary in-memory Profile for an // OpenAI-compatible chat-completions endpoint. // // It does not register global state, maintain a model catalog, or resolve // credentials. If APIKeyRequired is true, callers satisfy it with // RunRequest.APIKey or an explicit request ExecutionTargetOverride.APIKeyEnv. // Raw API keys do not belong in profiles. // // The function copies the ExtraParams map itself but does not recursively copy // nested values. Validation and a deep copy occur when NewEngine applies a // WithProfiles option containing the returned Profile. func OpenAICompatibleProfile(cfg OpenAICompatibleProfileConfig) Profile { return Profile{ ID: cfg.ID, BackendID: cfg.BackendID, Endpoint: cfg.Endpoint, Model: cfg.Model, Temperature: cfg.Temperature, MaxTokens: cfg.MaxTokens, TopP: cfg.TopP, TimeoutSeconds: cfg.TimeoutSeconds, ServiceTier: cfg.ServiceTier, ReasoningEffort: cfg.ReasoningEffort, APIKeyRequired: cfg.APIKeyRequired, ExtraParams: copyShallowAnyMap(cfg.ExtraParams), } } func copyShallowAnyMap(src map[string]any) map[string]any { if src == nil { return nil } out := make(map[string]any, len(src)) for k, v := range src { out[k] = v } return out } type memoryProfileRepository struct { profiles map[string]domain.ExecutionProfile } func newMemoryProfileRepository(profiles []Profile) (*memoryProfileRepository, error) { repo := &memoryProfileRepository{profiles: make(map[string]domain.ExecutionProfile, len(profiles))} for _, publicProfile := range profiles { prof, err := toDomainProfile(publicProfile) if err != nil { return nil, err } if _, exists := repo.profiles[prof.ID]; exists { return nil, fmt.Errorf("duplicate profile id %q", prof.ID) } repo.profiles[prof.ID] = prof } return repo, nil } func (r *memoryProfileRepository) GetProfile(_ context.Context, id string) (*domain.ExecutionProfile, error) { if r == nil { return nil, profile.ErrProfileNotFound } prof, ok := r.profiles[id] if !ok { return nil, profile.ErrProfileNotFound } prof.ExtraParams = copyAnyMap(prof.ExtraParams) return &prof, nil } func toDomainProfile(publicProfile Profile) (domain.ExecutionProfile, error) { extraParams, err := jsonvalue.CopyMap(publicProfile.ExtraParams) if err != nil { return domain.ExecutionProfile{}, err } prof := domain.ExecutionProfile{ ID: strings.TrimSpace(publicProfile.ID), BackendID: strings.TrimSpace(publicProfile.BackendID), Endpoint: publicProfile.Endpoint, Model: publicProfile.Model, Temperature: publicProfile.Temperature, MaxTokens: publicProfile.MaxTokens, TopP: publicProfile.TopP, TimeoutSeconds: publicProfile.TimeoutSeconds, ServiceTier: publicProfile.ServiceTier, ReasoningEffort: publicProfile.ReasoningEffort, APIKeyRequired: publicProfile.APIKeyRequired, ExtraParams: extraParams, } if err := validatePublicProfile(prof); err != nil { return domain.ExecutionProfile{}, err } return prof, nil } func validatePublicProfile(prof domain.ExecutionProfile) error { if strings.TrimSpace(prof.ID) == "" { return errors.New("id is required") } if strings.TrimSpace(prof.BackendID) == "" && strings.TrimSpace(prof.Endpoint) == "" { return errors.New("backend or endpoint is required") } if strings.TrimSpace(prof.Model) == "" { return errors.New("model is required") } if prof.Temperature < 0 || prof.Temperature > 2 { return errors.New("temperature must be between 0 and 2") } if prof.MaxTokens < 0 { return errors.New("max_tokens must be greater than or equal to 0") } if prof.TopP < 0 || prof.TopP > 1 { return errors.New("top_p must be between 0 and 1") } if prof.TimeoutSeconds < 0 { return errors.New("timeout_seconds must be greater than or equal to 0") } return nil }