1package typesafe23import (4 "fmt"5 "net/http"6 "net/url"7 "os"8 "strings"9 "time"10)1112// DefaultBaseURL is the hosted TypeSafe API root.13const DefaultBaseURL = "https://api.typesafe.ai"1415// DefaultModel is used when neither the request nor the client specifies a model.16const DefaultModel = "jev-latest"1718// Version is the SDK version sent in diagnostic request headers.19const Version = "0.2.0"2021type clientConfig struct {22 apiKey string23 baseURL string24 model string25 httpClient *http.Client26 timeout time.Duration27 retry RetryPolicy28 headers http.Header29}3031// Option configures a client. Use the With functions to construct options.32// Options are applied in order, after environment variables and defaults.33type Option func(*clientConfig) error3435func defaultConfig() clientConfig {36 return clientConfig{37 apiKey: strings.TrimSpace(os.Getenv("TYPESAFE_API_KEY")),38 baseURL: environmentOr("TYPESAFE_BASE_URL", DefaultBaseURL),39 model: environmentOr("TYPESAFE_DEFAULT_MODEL", DefaultModel),40 httpClient: &http.Client{},41 timeout: 10 * time.Second,42 retry: DefaultRetryPolicy(),43 headers: make(http.Header),44 }45}4647func environmentOr(key, fallback string) string {48 if value := strings.TrimSpace(os.Getenv(key)); value != "" {49 return value50 }51 return fallback52}5354// WithAPIKey sets the API key, overriding TYPESAFE_API_KEY.55func WithAPIKey(key string) Option {56 return func(config *clientConfig) error {57 config.apiKey = strings.TrimSpace(key)58 return nil59 }60}6162// WithBaseURL sets the API root, overriding TYPESAFE_BASE_URL. A path prefix is63// supported. The URL must use HTTP or HTTPS and contain no credentials, query,64// or fragment. Use HTTPS for remote services.65func WithBaseURL(baseURL string) Option {66 return func(config *clientConfig) error {67 config.baseURL = strings.TrimSpace(baseURL)68 return nil69 }70}7172// WithModel sets the default model, overriding TYPESAFE_DEFAULT_MODEL.73// A nonempty SystemOneRequest.Model takes precedence over this setting.74func WithModel(model string) Option {75 return func(config *clientConfig) error {76 config.model = strings.TrimSpace(model)77 return nil78 }79}8081// WithHTTPClient sets the HTTP client. The SDK takes a shallow copy and disables82// redirects; it does not mutate or close the caller's client or transport.83// A shorter HTTP client timeout also applies to each attempt.84func WithHTTPClient(client *http.Client) Option {85 return func(config *clientConfig) error {86 if client == nil {87 return fmt.Errorf("typesafe: HTTP client must not be nil")88 }89 config.httpClient = client90 return nil91 }92}9394// WithTimeout sets the positive timeout for one HTTP attempt, including reading95// the response body. The default is 10 seconds. Use a context deadline to bound96// the whole operation, including retries and backoff.97func WithTimeout(timeout time.Duration) Option {98 return func(config *clientConfig) error {99 if timeout <= 0 {100 return fmt.Errorf("typesafe: timeout must be positive")101 }102 config.timeout = timeout103 return nil104 }105}106107// WithMaxRetries sets retries after the initial attempt. Zero disables retries.108// It changes only MaxRetries in the current retry policy.109func WithMaxRetries(maxRetries int) Option {110 return func(config *clientConfig) error {111 if maxRetries < 0 {112 return fmt.Errorf("typesafe: max retries must not be negative")113 }114 config.retry.MaxRetries = maxRetries115 return nil116 }117}118119// WithRetryPolicy replaces the complete retry policy. Start from120// DefaultRetryPolicy to change selected settings while retaining the defaults.121func WithRetryPolicy(policy RetryPolicy) Option {122 return func(config *clientConfig) error {123 if err := policy.validate(); err != nil {124 return err125 }126 config.retry = policy127 return nil128 }129}130131// WithHeaders copies additional headers. Authentication, JSON content headers,132// SDK identity headers, and retry count are always controlled by the SDK.133func WithHeaders(headers http.Header) Option {134 cloned := canonicalHeaders(headers)135 return func(config *clientConfig) error {136 config.headers = cloned.Clone()137 return nil138 }139}140141func canonicalHeaders(headers http.Header) http.Header {142 result := make(http.Header, len(headers))143 for key, values := range headers {144 for _, value := range values {145 result.Add(key, value)146 }147 }148 return result149}150151func (config *clientConfig) validate() error {152 if config.apiKey == "" {153 return fmt.Errorf("typesafe: API key required; use WithAPIKey or TYPESAFE_API_KEY")154 }155 if strings.ContainsAny(config.apiKey, "\r\n") {156 return fmt.Errorf("typesafe: API key must not contain line breaks")157 }158 if config.model == "" {159 return fmt.Errorf("typesafe: default model must not be empty")160 }161 parsed, err := url.Parse(config.baseURL)162 if err != nil {163 return fmt.Errorf("typesafe: invalid base URL")164 }165 if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" {166 return fmt.Errorf("typesafe: base URL must be an HTTP(S) URL without credentials, query, or fragment")167 }168 config.baseURL = strings.TrimRight(config.baseURL, "/")169 return config.retry.validate()170}