132 lines
4.0 KiB
Go
132 lines
4.0 KiB
Go
package promptkit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
"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 := copyPublicJSONMap(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
|
|
}
|