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  }