okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
3b308f0
main
1// Package config defines CLI settings and validates values before they are 2// submitted to the daemon. The daemon owns persistence and credentials. 3package config 4 5import ( 6"errors" 7"net/url" 8"os" 9"path/filepath" 10"regexp" 11"strings" 12"unicode" 13) 14 15var providerNameRegex = regexp .MustCompile (`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$` ) 16 17// HomeDir returns the albedo configuration directory. 18func HomeDir ()string { 19if env := os .Getenv ("ALBEDO_HOME" );env != "" { 20abs ,err := filepath .Abs (env ) 21if err == nil { 22return abs 23 } 24return env 25 } 26home ,err := os .UserHomeDir () 27if err != nil { 28return ".albedo" 29 } 30return filepath .Join (home ,".albedo" ) 31} 32 33// Settings represents a provider configuration. Its api key lives in the 34// daemon's credential store. APIKey carries a newly entered draft value; 35// HasKey is presence metadata returned by the daemon. 36type Settings struct { 37ProfileName string `json:"-"` 38Effort * string `json:"effort,omitempty"` 39ImageEdge * int `json:"image_edge,omitempty"` 40AccountID * string `json:"account_id,omitempty"` 41Project * string `json:"project,omitempty"` 42Location * string `json:"location,omitempty"` 43Extension string `json:"extension,omitempty"` 44BaseURL string `json:"baseUrl,omitempty"` 45APIKey string `json:"apiKey,omitempty"` 46Model string `json:"model"` 47Protocol string `json:"protocol"` 48HasKey bool `json:"hasKey,omitempty"` 49} 50 51func validateModel (value string ) (string ,error ) { 52trimmed := strings .TrimSpace (value ) 53if trimmed == "" || len (trimmed )> 512 || strings .ContainsFunc (trimmed ,func (r rune )bool {return r < 0x20 || r == 0x7f }) { 54return "" ,errors .New ("model ID must be 1–512 characters and contain no control characters" ) 55 } 56return trimmed ,nil 57} 58 59// Validate checks whether the Settings instance is valid. 60func (s Settings )Validate () (Settings ,error ) { 61if s .Protocol != "responses" && s .Protocol != "chat_completions" { 62return s ,errors .New ("protocol must be responses or chat_completions" ) 63 } 64 65model ,err := validateModel (s .Model ) 66if err != nil { 67return s ,err 68 } 69 70ext := s .Extension 71if ext == "" { 72ext = "openai" 73 } 74 75var endpoint string 76if s .BaseURL != "" { 77endpoint ,err = ValidateEndpoint (s .BaseURL ) 78if err != nil { 79return s ,err 80 } 81 } 82 83if ext == "openai" && endpoint == "" { 84return s ,errors .New ("OpenAI provider needs an API base URL" ) 85 } 86 87if strings .ContainsFunc (s .APIKey ,func (r rune )bool {return unicode .IsSpace (r )|| r < 0x20 || r == 0x7f }) { 88return s ,errors .New ("API key must not contain spaces or control characters" ) 89 } 90 91return Settings { 92ProfileName :s .ProfileName ,Effort :s .Effort ,ImageEdge :s .ImageEdge ,AccountID :s .AccountID ,Project :s .Project ,Location :s .Location , 93Extension :ext , 94BaseURL :endpoint , 95APIKey :s .APIKey , 96HasKey :s .HasKey , 97Model :model , 98Protocol :s .Protocol , 99 },nil 100} 101 102// ValidateProviderName validates a profile name. 103func ValidateProviderName (value string ) (string ,error ) { 104name := strings .TrimSpace (value ) 105if ! providerNameRegex .MatchString (name ) { 106return "" ,errors .New ("provider name must start with a letter or number and use only letters, numbers, dots, underscores, or hyphens (1–64 characters)" ) 107 } 108return name ,nil 109} 110 111// ValidateEndpoint parses and validates an HTTP/HTTPS API endpoint without query, credentials, or fragment. 112func ValidateEndpoint (value string ) (string ,error ) { 113u ,err := url .Parse (strings .TrimSpace (value )) 114if err != nil || u .Scheme == "" || u .Host == "" { 115return "" ,errors .New ("enter an HTTP or HTTPS API base URL" ) 116 } 117if u .Scheme != "http" && u .Scheme != "https" { 118return "" ,errors .New ("enter an HTTP or HTTPS API base URL" ) 119 } 120if u .User != nil || u .RawQuery != "" || u .Fragment != "" { 121return "" ,errors .New ("use an HTTP or HTTPS API base URL without credentials, a query, or a fragment" ) 122 } 123return strings .TrimRight (u .String (),"/" ),nil 124} 125 126// Profiles represents the stored provider configuration. 127type Profiles struct { 128ETag string `json:"-"` 129Providers map [string ]Settings `json:"providers"` 130Active string `json:"active,omitempty"` 131}