github.com/anthropics/anthropic-sdk-go@v1.70.1/client.go (about) 1 // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. 2 3 package anthropic 4 5 import ( 6 "context" 7 "errors" 8 "fmt" 9 "net/http" 10 "os" 11 "slices" 12 "strings" 13 "time" 14 15 "github.com/anthropics/anthropic-sdk-go/config" 16 "github.com/anthropics/anthropic-sdk-go/internal/auth" 17 "github.com/anthropics/anthropic-sdk-go/internal/requestconfig" 18 "github.com/anthropics/anthropic-sdk-go/option" 19 "github.com/anthropics/anthropic-sdk-go/shared/constant" 20 ) 21 22 // Client creates a struct with services and top level methods that help with 23 // interacting with the anthropic API. You should not instantiate this client 24 // directly, and instead use the [NewClient] method instead. 25 type Client struct { 26 Options []option.RequestOption 27 Completions CompletionService 28 Messages MessageService 29 Models ModelService 30 Files FileService 31 Skills SkillService 32 Beta BetaService 33 } 34 35 // DefaultClientOptions walks the default credential chain per the 36 // cross-SDK credential precedence spec: 37 // 38 // 1. ANTHROPIC_API_KEY 39 // 2. ANTHROPIC_AUTH_TOKEN 40 // 3. Explicit profile via ANTHROPIC_PROFILE (surfaces the error if the 41 // named profile is missing — the user explicitly selected it) 42 // 4. Env-var federation (ANTHROPIC_FEDERATION_RULE_ID + 43 // ANTHROPIC_ORGANIZATION_ID + ANTHROPIC_IDENTITY_TOKEN_FILE / _TOKEN) 44 // 5. Fallback profile (active_config file or literal "default" — a 45 // quiet miss when absent, so a WIF-configured machine with a 46 // leftover default profile still uses WIF) 47 // 48 // When no source produces a credential, the first request fails with an 49 // [auth.NoCredentialsError]. If ANTHROPIC_PROFILE points at a missing or 50 // invalid profile, the first request instead fails with a wrapped 51 // profile-load error naming the profile. An explicit credential option 52 // passed to [NewClient] (e.g. [option.WithAPIKey] or [option.WithAuthToken]) 53 // suppresses both paths. Also honors ANTHROPIC_BASE_URL. 54 func DefaultClientOptions() []option.RequestOption { 55 defaults := []option.RequestOption{ 56 option.WithHTTPClient(defaultHTTPClient()), 57 option.WithEnvironmentProduction(), 58 } 59 if o, ok := os.LookupEnv("ANTHROPIC_BASE_URL"); ok { 60 defaults = append(defaults, option.WithBaseURL(o)) 61 } 62 63 statuses := []auth.CredentialSourceStatus{} 64 65 if v, ok := os.LookupEnv("ANTHROPIC_API_KEY"); ok && v != "" { 66 defaults = append(defaults, option.WithAPIKey(v)) 67 return defaults 68 } 69 statuses = append(statuses, auth.CredentialSourceStatus{ 70 Name: "ANTHROPIC_API_KEY env var", 71 State: auth.CredentialSourceNotSet, 72 }) 73 74 if v, ok := os.LookupEnv("ANTHROPIC_AUTH_TOKEN"); ok && v != "" { 75 defaults = append(defaults, option.WithAuthToken(v)) 76 return defaults 77 } 78 statuses = append(statuses, auth.CredentialSourceStatus{ 79 Name: "ANTHROPIC_AUTH_TOKEN env var", 80 State: auth.CredentialSourceNotSet, 81 }) 82 83 // Step 3: explicit profile via ANTHROPIC_PROFILE. The user named a 84 // specific profile, so a load failure is surfaced immediately — do 85 // not fall through to env federation or the fallback profile. 86 if profile, ok := os.LookupEnv("ANTHROPIC_PROFILE"); ok && profile != "" { 87 cfg, err := config.LoadProfile(config.DefaultDir(), profile) 88 if err != nil { 89 return append(defaults, explicitProfileErrorOption(profile, err)) 90 } 91 return append(defaults, option.WithConfig(cfg)) 92 } 93 94 // Step 4: env-var federation. Beats the fallback profile so a 95 // WIF-configured machine with a leftover default profile file still 96 // uses WIF. 97 envResult, envDetail, envState := auth.EnvCredentials() 98 if envResult != nil { 99 defaults = append(defaults, auth.WithAuthMiddleware(envResult.Provider)) 100 return defaults 101 } 102 envFederationStatus := auth.CredentialSourceStatus{ 103 Name: "env federation (ANTHROPIC_FEDERATION_RULE_ID + ANTHROPIC_ORGANIZATION_ID + ANTHROPIC_IDENTITY_TOKEN_FILE)", 104 State: envState, 105 Detail: envDetail, 106 } 107 108 // Step 5: fallback profile (active_config or literal "default"). A 109 // missing profile here is a quiet miss — fall through to the no- 110 // credentials aggregate. 111 fallbackStatus, fallbackOpt := tryLoadFallbackProfile() 112 if fallbackOpt != nil { 113 return append(defaults, fallbackOpt) 114 } 115 if o, ok := os.LookupEnv("ANTHROPIC_WEBHOOK_SIGNING_KEY"); ok { 116 defaults = append(defaults, option.WithWebhookKey(o)) 117 } 118 if o, ok := os.LookupEnv("ANTHROPIC_CUSTOM_HEADERS"); ok { 119 for _, line := range strings.Split(o, "\n") { 120 colon := strings.Index(line, ":") 121 if colon >= 0 { 122 defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:]))) 123 } 124 } 125 } 126 127 statuses = append(statuses, envFederationStatus, fallbackStatus) 128 defaults = append(defaults, noCredentialsSentinel(statuses)) 129 return defaults 130 } 131 132 // tryLoadFallbackProfile attempts the step-5 fallback profile lookup: 133 // active_config file, otherwise literal "default". A missing profile is 134 // reported as a silent-miss status (the caller will fall through to the 135 // no-credentials aggregate); any other load error is reported as a 136 // load-failure status so the user sees the specific OS error. 137 func tryLoadFallbackProfile() (auth.CredentialSourceStatus, option.RequestOption) { 138 cfg, err := config.LoadConfig() 139 if err != nil { 140 if errors.Is(err, os.ErrNotExist) { 141 return auth.CredentialSourceStatus{ 142 Name: "profile config file", 143 State: auth.CredentialSourceNotFound, 144 Detail: "run `ant auth login` to create one", 145 }, nil 146 } 147 return auth.CredentialSourceStatus{ 148 Name: "profile config file", 149 State: auth.CredentialSourceLoadFailed, 150 Detail: err.Error(), 151 }, nil 152 } 153 return auth.CredentialSourceStatus{Name: "profile config file"}, option.WithConfig(cfg) 154 } 155 156 // explicitProfileErrorOption installs a middleware that fails the request 157 // with the underlying load error, unless a caller-supplied credential 158 // option preempts the profile. Used when ANTHROPIC_PROFILE names a profile 159 // whose config file cannot be loaded. 160 func explicitProfileErrorOption(profile string, loadErr error) option.RequestOption { 161 profileErr := fmt.Errorf("ANTHROPIC_PROFILE=%q: %w", profile, loadErr) 162 return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 163 cfg := r 164 check := func(req *http.Request, next func(*http.Request) (*http.Response, error)) (*http.Response, error) { 165 if cfg.APIKey != "" || cfg.AuthToken != "" { 166 return next(req) 167 } 168 if req.Header.Get("Authorization") != "" || req.Header.Get("X-Api-Key") != "" { 169 return next(req) 170 } 171 return nil, profileErr 172 } 173 r.Middlewares = append(r.Middlewares, check) 174 return nil 175 }) 176 } 177 178 func noCredentialsSentinel(statuses []auth.CredentialSourceStatus) option.RequestOption { 179 preBuiltErr := &auth.NoCredentialsError{Sources: statuses} 180 return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { 181 cfg := r 182 check := func(req *http.Request, next func(*http.Request) (*http.Response, error)) (*http.Response, error) { 183 if cfg.APIKey != "" || cfg.AuthToken != "" { 184 return next(req) 185 } 186 if len(cfg.Middlewares) > 1 { 187 return next(req) 188 } 189 if req.Header.Get("Authorization") != "" || req.Header.Get("X-Api-Key") != "" { 190 return next(req) 191 } 192 return nil, preBuiltErr 193 } 194 r.Middlewares = append(r.Middlewares, check) 195 return nil 196 }) 197 } 198 199 // NewClient generates a new client with the default option read from the 200 // environment (ANTHROPIC_API_KEY, ANTHROPIC_WEBHOOK_SIGNING_KEY, ANTHROPIC_AUTH_TOKEN, 201 // ANTHROPIC_BASE_URL). The option passed in as arguments are applied after these 202 // default arguments, and all option will be passed down to the services and requests 203 // that this client makes. 204 // 205 // Pass [option.WithoutEnvironmentDefaults] to skip the environment-based 206 // credential autoload entirely (only the hardcoded production base-URL 207 // default is kept). Use this when the caller does its own credential 208 // resolution and wants the SDK to contribute nothing from the environment. 209 func NewClient(opts ...option.RequestOption) (r Client) { 210 var defaults []option.RequestOption 211 if option.HasWithoutEnvironmentDefaults(opts) { 212 defaults = []option.RequestOption{option.WithEnvironmentProduction()} 213 } else { 214 defaults = DefaultClientOptions() 215 } 216 opts = append(defaults, opts...) 217 218 r = Client{Options: opts} 219 220 r.Completions = NewCompletionService(opts...) 221 r.Messages = NewMessageService(opts...) 222 r.Models = NewModelService(opts...) 223 r.Files = NewFileService(opts...) 224 r.Skills = NewSkillService(opts...) 225 r.Beta = NewBetaService(opts...) 226 227 return 228 } 229 230 // Execute makes a request with the given context, method, URL, request params, 231 // response, and request options. This is useful for hitting undocumented endpoints 232 // while retaining the base URL, auth, retries, and other options from the client. 233 // 234 // If a byte slice or an [io.Reader] is supplied to params, it will be used as-is 235 // for the request body. 236 // 237 // The params is by default serialized into the body using [encoding/json]. If your 238 // type implements a MarshalJSON function, it will be used instead to serialize the 239 // request. If a URLQuery method is implemented, the returned [url.Values] will be 240 // used as query strings to the url. 241 // 242 // If your params struct uses [param.Field], you must provide either [MarshalJSON], 243 // [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a 244 // struct uses [param.Field] without specifying how it is serialized. 245 // 246 // Any "…Params" object defined in this library can be used as the request 247 // argument. Note that 'path' arguments will not be forwarded into the url. 248 // 249 // The response body will be deserialized into the res variable, depending on its 250 // type: 251 // 252 // - A pointer to a [*http.Response] is populated by the raw response. 253 // - A pointer to a byte array will be populated with the contents of the request 254 // body. 255 // - A pointer to any other type uses this library's default JSON decoding, which 256 // respects UnmarshalJSON if it is defined on the type. 257 // - A nil value will not read the response body. 258 // 259 // For even greater flexibility, see [option.WithResponseInto] and 260 // [option.WithResponseBodyInto]. 261 func (r *Client) Execute(ctx context.Context, method string, path string, params any, res any, opts ...option.RequestOption) error { 262 opts = slices.Concat(r.Options, opts) 263 return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...) 264 } 265 266 // Get makes a GET request with the given URL, params, and optionally deserializes 267 // to a response. See [Execute] documentation on the params and response. 268 func (r *Client) Get(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { 269 return r.Execute(ctx, http.MethodGet, path, params, res, opts...) 270 } 271 272 // Post makes a POST request with the given URL, params, and optionally 273 // deserializes to a response. See [Execute] documentation on the params and 274 // response. 275 func (r *Client) Post(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { 276 return r.Execute(ctx, http.MethodPost, path, params, res, opts...) 277 } 278 279 // Put makes a PUT request with the given URL, params, and optionally deserializes 280 // to a response. See [Execute] documentation on the params and response. 281 func (r *Client) Put(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { 282 return r.Execute(ctx, http.MethodPut, path, params, res, opts...) 283 } 284 285 // Patch makes a PATCH request with the given URL, params, and optionally 286 // deserializes to a response. See [Execute] documentation on the params and 287 // response. 288 func (r *Client) Patch(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { 289 return r.Execute(ctx, http.MethodPatch, path, params, res, opts...) 290 } 291 292 // Delete makes a DELETE request with the given URL, params, and optionally 293 // deserializes to a response. See [Execute] documentation on the params and 294 // response. 295 func (r *Client) Delete(ctx context.Context, path string, params any, res any, opts ...option.RequestOption) error { 296 return r.Execute(ctx, http.MethodDelete, path, params, res, opts...) 297 } 298 299 // CalculateNonStreamingTimeout calculates the appropriate timeout for a non-streaming request 300 // based on the maximum number of tokens and the model's non-streaming token limit 301 func CalculateNonStreamingTimeout(maxTokens int, model Model, opts []option.RequestOption) (time.Duration, error) { 302 preCfg, err := requestconfig.PreRequestOptions(opts...) 303 if err != nil { 304 return 0, fmt.Errorf("error applying request options: %w", err) 305 } 306 // if the user has set a specific request timeout, use that 307 if preCfg.RequestTimeout != 0 { 308 return preCfg.RequestTimeout, nil 309 } 310 311 maximumTime := time.Hour // 1 hour 312 defaultTime := 10 * time.Minute 313 314 expectedTime := time.Duration(float64(maximumTime) * float64(maxTokens) / 128000.0) 315 316 // If the model has a non-streaming token limit and max_tokens exceeds it, 317 // or if the expected time exceeds default time, require streaming 318 maxNonStreamingTokens, hasLimit := constant.ModelNonStreamingTokens[string(model)] 319 if expectedTime > defaultTime || (hasLimit && maxTokens > maxNonStreamingTokens) { 320 return 0, fmt.Errorf("streaming is required for operations that may take longer than 10 minutes") 321 } 322 323 return defaultTime, nil 324 }