github.com/anthropics/anthropic-sdk-go@v1.70.1/option/requestoption.go (about)

     1  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
     2  
     3  package option
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/anthropics/anthropic-sdk-go/config"
    18  	"github.com/anthropics/anthropic-sdk-go/internal/auth"
    19  	"github.com/anthropics/anthropic-sdk-go/internal/requestconfig"
    20  	"github.com/tidwall/sjson"
    21  )
    22  
    23  // IdentityTokenFunc returns a fresh JWT identity token (e.g. from SPIFFE/SPIRE,
    24  // the GCP metadata server, or any other OIDC-compatible provider). It is
    25  // invoked once per federation token exchange; implementations should be safe
    26  // for concurrent use.
    27  type IdentityTokenFunc func(ctx context.Context) (string, error)
    28  
    29  // FederationOptions configures a federation-based token exchange.
    30  type FederationOptions struct {
    31  	// FederationRuleID identifies the OidcFederationRule governing this
    32  	// exchange. Required; must be a tagged ID with the "fdrl_" prefix.
    33  	FederationRuleID string
    34  	// OrganizationID is the UUID of the Anthropic organization the
    35  	// federation rule belongs to. Required.
    36  	OrganizationID string
    37  	// ServiceAccountID is an optional expected-target check for federation
    38  	// rules with target_type=SERVICE_ACCOUNT. Must be a tagged ID with the
    39  	// "svac_" prefix. Omit for target_type=USER rules, where the principal
    40  	// is derived from the JWT claims.
    41  	ServiceAccountID string
    42  	// WorkspaceID is an optional `wrkspc_*` tagged ID, or the literal
    43  	// "default" to scope the token to the organization's default workspace.
    44  	// When omitted the server picks the rule's sole enabled workspace, else
    45  	// the org default if the rule covers it. Required when the rule enables
    46  	// more than one non-default workspace, or to target a specific workspace
    47  	// other than the one the server would pick. The minted token is
    48  	// workspace-scoped: per-request workspace selection (the
    49  	// anthropic-workspace-id header) is not supported for federation
    50  	// tokens — switching workspaces requires a new token exchange with a
    51  	// different WorkspaceID.
    52  	WorkspaceID string
    53  }
    54  
    55  // identityTokenFuncAdapter adapts an [IdentityTokenFunc] to the internal
    56  // [auth.IdentityTokenProvider] interface without leaking the internal type.
    57  type identityTokenFuncAdapter struct {
    58  	fn IdentityTokenFunc
    59  }
    60  
    61  func (a identityTokenFuncAdapter) GetIdentityToken(ctx context.Context) (string, error) {
    62  	if a.fn == nil {
    63  		return "", fmt.Errorf("option: IdentityTokenFunc is nil")
    64  	}
    65  	return a.fn(ctx)
    66  }
    67  
    68  // errOption returns a [RequestOption] whose Apply unconditionally returns
    69  // err. Used to defer validation errors from option constructors so they
    70  // surface on the first request rather than at option-construction time.
    71  func errOption(err error) RequestOption {
    72  	return requestconfig.RequestOptionFunc(func(*requestconfig.RequestConfig) error {
    73  		return err
    74  	})
    75  }
    76  
    77  // IdentityTokenFile returns an [IdentityTokenFunc] that reads a JWT from path
    78  // on each invocation. Use it for Kubernetes projected service account tokens
    79  // and similar rotating on-disk credentials — the file is re-read on every
    80  // federation exchange so rotations are picked up automatically. Surrounding
    81  // whitespace is trimmed; an empty file is treated as an error.
    82  //
    83  // Passing this as the provider to [WithFederationTokenProvider] takes
    84  // precedence over the ANTHROPIC_IDENTITY_TOKEN_FILE environment variable:
    85  // explicit options always beat env-var auto-discovery. The env var only
    86  // applies when the client is constructed without a [WithFederationTokenProvider]
    87  // option (or equivalent).
    88  //
    89  // The returned function is safe for concurrent use.
    90  //
    91  // Typical usage:
    92  //
    93  //	client := anthropic.NewClient(
    94  //	    option.WithFederationTokenProvider(
    95  //	        option.IdentityTokenFile("/var/run/secrets/anthropic.com/token"),
    96  //	        option.FederationOptions{
    97  //	            FederationRuleID: "fdrl_...",
    98  //	            OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
    99  //	        },
   100  //	    ),
   101  //	)
   102  func IdentityTokenFile(path string) IdentityTokenFunc {
   103  	return (&auth.IdentityTokenFile{Path: path}).GetIdentityToken
   104  }
   105  
   106  // WithFederationTokenProvider returns a [RequestOption] that authenticates
   107  // requests using workload identity federation, exchanging a caller-supplied
   108  // identity token for a short-lived Anthropic access token. Use this to
   109  // integrate custom OIDC token sources (SPIFFE/SPIRE, cloud provider SDKs,
   110  // etc.) without staging the token through a file.
   111  //
   112  // provider is called on each token exchange to fetch a fresh JWT.
   113  // opts.FederationRuleID and opts.OrganizationID are required.
   114  //
   115  // The [auth.TokenCache] built by [auth.WithAuthMiddleware] is shared across
   116  // requests, so a token is only re-exchanged when it enters the refresh window.
   117  func WithFederationTokenProvider(provider IdentityTokenFunc, opts FederationOptions) RequestOption {
   118  	switch {
   119  	case provider == nil:
   120  		return errOption(fmt.Errorf("option: WithFederationTokenProvider: provider is nil"))
   121  	case opts.FederationRuleID == "":
   122  		return errOption(fmt.Errorf("option: WithFederationTokenProvider: FederationRuleID is required"))
   123  	case opts.OrganizationID == "":
   124  		return errOption(fmt.Errorf("option: WithFederationTokenProvider: OrganizationID is required"))
   125  	}
   126  	tokenProvider := auth.NewOIDCFederationCredentials(auth.OIDCFederationConfig{
   127  		IdentityProvider: identityTokenFuncAdapter{fn: provider},
   128  		FederationRuleID: opts.FederationRuleID,
   129  		OrganizationID:   opts.OrganizationID,
   130  		ServiceAccountID: opts.ServiceAccountID,
   131  		WorkspaceID:      opts.WorkspaceID,
   132  	})
   133  	return auth.WithAuthMiddleware(tokenProvider)
   134  }
   135  
   136  // RequestOption is an option for the requests made by the anthropic API Client
   137  // which can be supplied to clients, services, and methods. You can read more about this functional
   138  // options pattern in our [README].
   139  //
   140  // [README]: https://pkg.go.dev/github.com/anthropics/anthropic-sdk-go#readme-requestoptions
   141  type RequestOption = requestconfig.RequestOption
   142  
   143  // WithBaseURL returns a RequestOption that sets the BaseURL for the client.
   144  //
   145  // For security reasons, ensure that the base URL is trusted.
   146  func WithBaseURL(base string) RequestOption {
   147  	u, err := url.Parse(base)
   148  	if err == nil && u.Path != "" && !strings.HasSuffix(u.Path, "/") {
   149  		u.Path += "/"
   150  	}
   151  
   152  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   153  		if err != nil {
   154  			return fmt.Errorf("requestoption: WithBaseURL failed to parse url %s", err)
   155  		}
   156  
   157  		r.BaseURL = u
   158  		return nil
   159  	})
   160  }
   161  
   162  // HTTPClient is primarily used to describe an [*http.Client], but also
   163  // supports custom implementations.
   164  //
   165  // For bespoke implementations, prefer using an [*http.Client] with a
   166  // custom transport. See [http.RoundTripper] for further information.
   167  type HTTPClient interface {
   168  	Do(*http.Request) (*http.Response, error)
   169  }
   170  
   171  // WithHTTPClient returns a RequestOption that changes the underlying http client used to make this
   172  // request, which by default is [http.DefaultClient].
   173  //
   174  // For custom uses cases, it is recommended to provide an [*http.Client] with a custom
   175  // [http.RoundTripper] as its transport, rather than directly implementing [HTTPClient].
   176  func WithHTTPClient(client HTTPClient) RequestOption {
   177  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   178  		if client == nil {
   179  			return fmt.Errorf("requestoption: custom http client cannot be nil")
   180  		}
   181  
   182  		if c, ok := client.(*http.Client); ok {
   183  			// Prefer the native client if possible.
   184  			r.HTTPClient = c
   185  			r.CustomHTTPDoer = nil
   186  		} else {
   187  			r.CustomHTTPDoer = client
   188  		}
   189  
   190  		return nil
   191  	})
   192  }
   193  
   194  // MiddlewareNext is a function which is called by a middleware to pass an HTTP request
   195  // to the next stage in the middleware chain.
   196  type MiddlewareNext = func(*http.Request) (*http.Response, error)
   197  
   198  // Middleware is a function which intercepts HTTP requests, processing or modifying
   199  // them, and then passing the request to the next middleware or handler
   200  // in the chain by calling the provided MiddlewareNext function.
   201  type Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error)
   202  
   203  // WithMiddleware returns a RequestOption that applies the given middleware
   204  // to the requests made. Each middleware will execute in the order they were given.
   205  func WithMiddleware(middlewares ...Middleware) RequestOption {
   206  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   207  		r.Middlewares = append(r.Middlewares, middlewares...)
   208  		return nil
   209  	})
   210  }
   211  
   212  // WithMaxRetries returns a RequestOption that sets the maximum number of retries that the client
   213  // attempts to make. When given 0, the client only makes one request. By
   214  // default, the client retries two times.
   215  //
   216  // WithMaxRetries panics when retries is negative.
   217  func WithMaxRetries(retries int) RequestOption {
   218  	if retries < 0 {
   219  		panic("option: cannot have fewer than 0 retries")
   220  	}
   221  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   222  		r.MaxRetries = retries
   223  		return nil
   224  	})
   225  }
   226  
   227  // WithHeader returns a RequestOption that sets the header value to the associated key. It overwrites
   228  // any value if there was one already present.
   229  func WithHeader(key, value string) RequestOption {
   230  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   231  		r.Request.Header.Set(key, value)
   232  		return nil
   233  	})
   234  }
   235  
   236  // WithHeaderAdd returns a RequestOption that adds the header value to the associated key. It appends
   237  // onto any existing values.
   238  func WithHeaderAdd(key, value string) RequestOption {
   239  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   240  		r.Request.Header.Add(key, value)
   241  		return nil
   242  	})
   243  }
   244  
   245  // WithHeaderDel returns a RequestOption that deletes the header value(s) associated with the given key.
   246  func WithHeaderDel(key string) RequestOption {
   247  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   248  		r.Request.Header.Del(key)
   249  		return nil
   250  	})
   251  }
   252  
   253  // WithQuery returns a RequestOption that sets the query value to the associated key. It overwrites
   254  // any value if there was one already present.
   255  func WithQuery(key, value string) RequestOption {
   256  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   257  		query := r.Request.URL.Query()
   258  		query.Set(key, value)
   259  		r.Request.URL.RawQuery = query.Encode()
   260  		return nil
   261  	})
   262  }
   263  
   264  // WithQueryAdd returns a RequestOption that adds the query value to the associated key. It appends
   265  // onto any existing values.
   266  func WithQueryAdd(key, value string) RequestOption {
   267  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   268  		query := r.Request.URL.Query()
   269  		query.Add(key, value)
   270  		r.Request.URL.RawQuery = query.Encode()
   271  		return nil
   272  	})
   273  }
   274  
   275  // WithQueryDel returns a RequestOption that deletes the query value(s) associated with the key.
   276  func WithQueryDel(key string) RequestOption {
   277  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   278  		query := r.Request.URL.Query()
   279  		query.Del(key)
   280  		r.Request.URL.RawQuery = query.Encode()
   281  		return nil
   282  	})
   283  }
   284  
   285  // WithJSONSet returns a RequestOption that sets the body's JSON value associated with the key.
   286  // The key accepts a string as defined by the [sjson format].
   287  //
   288  // [sjson format]: https://github.com/tidwall/sjson
   289  func WithJSONSet(key string, value any) RequestOption {
   290  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) {
   291  		var b []byte
   292  
   293  		if r.Body == nil {
   294  			b, err = sjson.SetBytes(nil, key, value)
   295  			if err != nil {
   296  				return err
   297  			}
   298  		} else if buffer, ok := r.Body.(*bytes.Buffer); ok {
   299  			b = buffer.Bytes()
   300  			b, err = sjson.SetBytes(b, key, value)
   301  			if err != nil {
   302  				return err
   303  			}
   304  		} else {
   305  			return fmt.Errorf("cannot use WithJSONSet on a body that is not serialized as *bytes.Buffer")
   306  		}
   307  
   308  		r.Body = bytes.NewBuffer(b)
   309  		return nil
   310  	})
   311  }
   312  
   313  // WithJSONDel returns a RequestOption that deletes the body's JSON value associated with the key.
   314  // The key accepts a string as defined by the [sjson format].
   315  //
   316  // [sjson format]: https://github.com/tidwall/sjson
   317  func WithJSONDel(key string) RequestOption {
   318  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) {
   319  		if buffer, ok := r.Body.(*bytes.Buffer); ok {
   320  			b := buffer.Bytes()
   321  			b, err = sjson.DeleteBytes(b, key)
   322  			if err != nil {
   323  				return err
   324  			}
   325  			r.Body = bytes.NewBuffer(b)
   326  			return nil
   327  		}
   328  
   329  		return fmt.Errorf("cannot use WithJSONDel on a body that is not serialized as *bytes.Buffer")
   330  	})
   331  }
   332  
   333  // WithResponseBodyInto returns a RequestOption that overwrites the deserialization target with
   334  // the given destination. If provided, we don't deserialize into the default struct.
   335  func WithResponseBodyInto(dst any) RequestOption {
   336  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   337  		r.ResponseBodyInto = dst
   338  		return nil
   339  	})
   340  }
   341  
   342  // WithResponseInto returns a RequestOption that copies the [*http.Response] into the given address.
   343  func WithResponseInto(dst **http.Response) RequestOption {
   344  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   345  		r.ResponseInto = dst
   346  		return nil
   347  	})
   348  }
   349  
   350  // WithRequestBody returns a RequestOption that provides a custom serialized body with the given
   351  // content type.
   352  //
   353  // body accepts an io.Reader or raw []bytes.
   354  func WithRequestBody(contentType string, body any) RequestOption {
   355  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   356  		if reader, ok := body.(io.Reader); ok {
   357  			r.Body = reader
   358  			return r.Apply(WithHeader("Content-Type", contentType))
   359  		}
   360  
   361  		if b, ok := body.([]byte); ok {
   362  			r.Body = bytes.NewBuffer(b)
   363  			return r.Apply(WithHeader("Content-Type", contentType))
   364  		}
   365  
   366  		return fmt.Errorf("body must be a byte slice or implement io.Reader")
   367  	})
   368  }
   369  
   370  // WithRequestTimeout returns a RequestOption that sets the timeout for
   371  // each request attempt. This should be smaller than the timeout defined in
   372  // the context, which spans all retries.
   373  //
   374  // An attempt that exceeds this timeout is retried (see [WithMaxRetries]) for as
   375  // long as the context passed to the method is not done, so a call can take up to
   376  // roughly (retries + 1) * dur plus backoff before it fails with context.DeadlineExceeded.
   377  func WithRequestTimeout(dur time.Duration) RequestOption {
   378  	// we need this to be a PreRequestOptionFunc so that it can be applied at the endpoint level
   379  	// see: CalculateNonStreamingTimeout
   380  	return requestconfig.PreRequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   381  		r.RequestTimeout = dur
   382  		return nil
   383  	})
   384  }
   385  
   386  // withConfigClientKey mirrors auth.clientKey: one auth.TokenCache per transport.
   387  type withConfigClientKey struct {
   388  	httpClient *http.Client
   389  	customDoer requestconfig.HTTPDoer
   390  }
   391  
   392  // WithConfig returns a [RequestOption] that authenticates requests using
   393  // the given [config.Config].
   394  //
   395  // Credential resolution is deferred to a request-time middleware so a
   396  // later [WithAPIKey] / [WithAuthToken] (tier-1 explicit-credential
   397  // precedence) can preempt the profile — even when the profile's
   398  // credentials are missing or broken. The same escape-hatch shape as
   399  // [explicitProfileErrorOption] applies: if the final RequestConfig has a
   400  // static APIKey/AuthToken, or the request already carries an
   401  // Authorization / X-Api-Key header, the profile is treated as shadowed
   402  // and resolution is skipped entirely.
   403  //
   404  // The config's non-credential fields (BaseURL, WorkspaceID) apply at
   405  // option-Apply time and do not depend on credential resolution. BaseURL
   406  // only takes effect when no explicit [WithBaseURL] is present.
   407  //
   408  // When a static credential shadows the profile, a one-shot warning is
   409  // emitted so users migrating from API keys can tell why their profile is
   410  // being ignored. Use [WithConfigQuiet] to suppress that warning when the
   411  // caller has its own, richer diagnostic.
   412  func WithConfig(cfg *config.Config) RequestOption {
   413  	return withConfig(cfg, false)
   414  }
   415  
   416  // WithConfigQuiet is identical to [WithConfig] but suppresses the one-shot
   417  // "static credential shadows profile" warning. Intended for callers (such as
   418  // the CLI) that detect the same condition themselves and emit a more
   419  // specific multi-auth diagnostic, so users don't see two warnings for the
   420  // same thing.
   421  func WithConfigQuiet(cfg *config.Config) RequestOption {
   422  	return withConfig(cfg, true)
   423  }
   424  
   425  // WithProfile returns a [RequestOption] that loads the named profile from
   426  // the default config directory (see [config.DefaultDir]) and authenticates
   427  // requests using it — equivalent to setting ANTHROPIC_PROFILE=name and
   428  // constructing a zero-config client. Shorthand for:
   429  //
   430  //	cfg, err := config.LoadProfile(config.DefaultDir(), name)
   431  //	... option.WithConfig(cfg)
   432  //
   433  // If the profile cannot be loaded, the error is deferred to the first
   434  // request and is preempted by a static credential ([WithAPIKey] /
   435  // [WithAuthToken]) — the same escape hatch as the ANTHROPIC_PROFILE env
   436  // path. As with [WithConfig], a static credential shadows the profile at
   437  // request time and a one-shot warning is emitted.
   438  func WithProfile(name string) RequestOption {
   439  	if name == "" {
   440  		return errOption(fmt.Errorf("option: WithProfile: profile name is empty"))
   441  	}
   442  	cfg, err := config.LoadProfile(config.DefaultDir(), name)
   443  	if err != nil {
   444  		loadErr := fmt.Errorf("option: WithProfile(%q): %w", name, err)
   445  		return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   446  			rc := r
   447  			check := func(req *http.Request, next func(*http.Request) (*http.Response, error)) (*http.Response, error) {
   448  				if rc.APIKey != "" || rc.AuthToken != "" {
   449  					return next(req)
   450  				}
   451  				if req.Header.Get("Authorization") != "" || req.Header.Get("X-Api-Key") != "" {
   452  					return next(req)
   453  				}
   454  				return nil, loadErr
   455  			}
   456  			r.Middlewares = append(r.Middlewares, check)
   457  			return nil
   458  		})
   459  	}
   460  	return withConfig(cfg, false)
   461  }
   462  
   463  // joinedOption is the [RequestOption] returned by [Join]. It is a distinct
   464  // type (rather than a RequestOptionFunc closure) so that
   465  // [HasWithoutEnvironmentDefaults] can look inside it.
   466  type joinedOption []RequestOption
   467  
   468  func (opts joinedOption) Apply(r *requestconfig.RequestConfig) error {
   469  	return r.Apply(opts...)
   470  }
   471  
   472  // Join returns a single [RequestOption] that applies opts in order, at the
   473  // position the joined option occupies in the caller's option list. It is
   474  // equivalent to passing opts individually at that position, and lets a
   475  // function that configures several aspects of a client return one option:
   476  //
   477  //	func WithPlatform(cfg Config) option.RequestOption {
   478  //		return option.Join(
   479  //			option.WithoutEnvironmentDefaults(),
   480  //			option.WithBaseURL(cfg.URL),
   481  //			option.WithMiddleware(cfg.middleware()),
   482  //		)
   483  //	}
   484  //
   485  // A [WithoutEnvironmentDefaults] marker nested inside a Join (at any depth)
   486  // is still recognized by anthropic.NewClient.
   487  func Join(opts ...RequestOption) RequestOption {
   488  	return joinedOption(opts)
   489  }
   490  
   491  // withoutEnvironmentDefaultsOption is the marker type returned by
   492  // [WithoutEnvironmentDefaults]. Its Apply is a no-op; it is detected by
   493  // [HasWithoutEnvironmentDefaults] before options are applied.
   494  type withoutEnvironmentDefaultsOption struct{}
   495  
   496  func (withoutEnvironmentDefaultsOption) Apply(*requestconfig.RequestConfig) error { return nil }
   497  
   498  // WithoutEnvironmentDefaults returns a marker [RequestOption] that, when
   499  // passed to anthropic.NewClient, causes it to skip the environment-based
   500  // credential autoload performed by anthropic.DefaultClientOptions
   501  // (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_PROFILE, env
   502  // federation, fallback profile, ANTHROPIC_BASE_URL). The hardcoded
   503  // production base-URL default is still applied so callers that supply only
   504  // credentials get a working client.
   505  //
   506  // Intended for callers that perform their own credential resolution (such
   507  // as the CLI) and want full control over which auth source is used, without
   508  // the SDK's autoloader contributing a second [WithConfig] that would emit a
   509  // duplicate shadow-warning.
   510  func WithoutEnvironmentDefaults() RequestOption {
   511  	return withoutEnvironmentDefaultsOption{}
   512  }
   513  
   514  // HasWithoutEnvironmentDefaults reports whether opts contains a
   515  // [WithoutEnvironmentDefaults] marker, looking inside options combined with
   516  // [Join] at any depth. Used by anthropic.NewClient to decide whether to
   517  // prepend anthropic.DefaultClientOptions.
   518  func HasWithoutEnvironmentDefaults(opts []RequestOption) bool {
   519  	for _, o := range opts {
   520  		switch o := o.(type) {
   521  		case withoutEnvironmentDefaultsOption:
   522  			return true
   523  		case joinedOption:
   524  			if HasWithoutEnvironmentDefaults(o) {
   525  				return true
   526  			}
   527  		}
   528  	}
   529  	return false
   530  }
   531  
   532  func withConfig(cfg *config.Config, quiet bool) RequestOption {
   533  	var (
   534  		shadowOnce  sync.Once
   535  		resolveOnce sync.Once
   536  		provider    auth.TokenProvider
   537  		resolveErr  error
   538  
   539  		cacheMu sync.Mutex
   540  		cacheBy = map[withConfigClientKey]*auth.TokenCache{}
   541  	)
   542  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   543  		// Non-credential config — applied unconditionally so a profile's
   544  		// base_url / workspace_id are honored even when its credentials
   545  		// are shadowed by a later WithAPIKey or fail to resolve.
   546  		if cfg.BaseURL != "" && r.BaseURL == nil {
   547  			if err := WithBaseURL(cfg.BaseURL).Apply(r); err != nil {
   548  				return err
   549  			}
   550  		}
   551  		// For federation profiles workspace_id is sent in the jwt-bearer
   552  		// exchange body, not as a request header (the minted token is
   553  		// already workspace-scoped, so the header would be ignored).
   554  		isFederation := cfg.AuthenticationInfo != nil &&
   555  			cfg.AuthenticationInfo.Type == config.AuthenticationTypeOIDCFederation
   556  		if cfg.WorkspaceID != "" && !isFederation {
   557  			if err := WithHeader("anthropic-workspace-id", cfg.WorkspaceID).Apply(r); err != nil {
   558  				return err
   559  			}
   560  		}
   561  
   562  		// Both checks below are request-time middlewares so they observe
   563  		// the RequestConfig after ALL options have applied — order of
   564  		// WithConfig vs WithAPIKey/WithAuthToken in the caller's option
   565  		// list doesn't matter.
   566  		rc := r
   567  		if !quiet {
   568  			shadowCheck := func(req *http.Request, next func(*http.Request) (*http.Response, error)) (*http.Response, error) {
   569  				shadowOnce.Do(func() {
   570  					switch {
   571  					case rc.APIKey != "":
   572  						auth.WarnConfigShadowed("ANTHROPIC_API_KEY", detectShadowSource("ANTHROPIC_API_KEY"))
   573  					case rc.AuthToken != "":
   574  						auth.WarnConfigShadowed("ANTHROPIC_AUTH_TOKEN", detectShadowSource("ANTHROPIC_AUTH_TOKEN"))
   575  					}
   576  				})
   577  				return next(req)
   578  			}
   579  			r.Middlewares = append(r.Middlewares, shadowCheck)
   580  		}
   581  
   582  		credCheck := func(req *http.Request, next func(*http.Request) (*http.Response, error)) (*http.Response, error) {
   583  			if rc.APIKey != "" || rc.AuthToken != "" {
   584  				return next(req)
   585  			}
   586  			if req.Header.Get("Authorization") != "" || req.Header.Get("X-Api-Key") != "" {
   587  				return next(req)
   588  			}
   589  			resolveOnce.Do(func() {
   590  				result, err := auth.ResolveCredentials(cfg)
   591  				if err != nil {
   592  					resolveErr = err
   593  					return
   594  				}
   595  				provider = result.Provider
   596  			})
   597  			if resolveErr != nil {
   598  				return nil, resolveErr
   599  			}
   600  
   601  			handler := rc.HTTPClient.Do
   602  			if rc.CustomHTTPDoer != nil {
   603  				handler = rc.CustomHTTPDoer.Do
   604  			}
   605  			key := withConfigClientKey{httpClient: rc.HTTPClient, customDoer: rc.CustomHTTPDoer}
   606  			cacheMu.Lock()
   607  			cache, ok := cacheBy[key]
   608  			if !ok {
   609  				cache = auth.NewTokenCache(provider, handler)
   610  				cacheBy[key] = cache
   611  			}
   612  			cacheMu.Unlock()
   613  			return auth.NewMiddleware(cache, rc)(req, next)
   614  		}
   615  		r.Middlewares = append(r.Middlewares, credCheck)
   616  		return nil
   617  	})
   618  }
   619  
   620  // detectShadowSource classifies where the shadowing static credential
   621  // came from by inspecting whether the named env var is set. When the env
   622  // var has a non-empty value the source is [auth.ConfigShadowFromEnv];
   623  // otherwise the credential must have come from an explicit option call.
   624  func detectShadowSource(envVar string) auth.ConfigShadowSource {
   625  	if v, ok := os.LookupEnv(envVar); ok && v != "" {
   626  		return auth.ConfigShadowFromEnv
   627  	}
   628  	return auth.ConfigShadowFromOption
   629  }
   630  
   631  // WithEnvironmentProduction returns a RequestOption that sets the current
   632  // environment to be the "production" environment. An environment specifies which base URL
   633  // to use by default.
   634  func WithEnvironmentProduction() RequestOption {
   635  	return requestconfig.WithDefaultBaseURL("https://api.anthropic.com/")
   636  }
   637  
   638  // WithAPIKey returns a RequestOption that sets the client setting "api_key".
   639  func WithAPIKey(value string) RequestOption {
   640  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   641  		r.APIKey = value
   642  		return r.Apply(WithHeader("X-Api-Key", r.APIKey))
   643  	})
   644  }
   645  
   646  // WithAuthToken returns a RequestOption that sets the client setting "auth_token".
   647  func WithAuthToken(value string) RequestOption {
   648  	return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   649  		r.AuthToken = value
   650  		return r.Apply(WithHeader("authorization", fmt.Sprintf("Bearer %s", r.AuthToken)))
   651  	})
   652  }
   653  
   654  // WithWebhookKey returns a RequestOption that sets the client setting "webhook_key".
   655  func WithWebhookKey(value string) RequestOption {
   656  	return requestconfig.PreRequestOptionFunc(func(r *requestconfig.RequestConfig) error {
   657  		r.WebhookKey = value
   658  		return nil
   659  	})
   660  }