@@ -4,13 +4,16 @@ package anthropic
44
55import (
66 "context"
7+ "errors"
78 "fmt"
89 "net/http"
910 "os"
1011 "slices"
11- "time"
1212 "strings"
13+ "time"
1314
15+ "github.com/anthropics/anthropic-sdk-go/config"
16+ "github.com/anthropics/anthropic-sdk-go/internal/auth"
1417 "github.com/anthropics/anthropic-sdk-go/internal/requestconfig"
1518 "github.com/anthropics/anthropic-sdk-go/option"
1619 "github.com/anthropics/anthropic-sdk-go/shared/constant"
@@ -27,9 +30,25 @@ type Client struct {
2730 Beta BetaService
2831}
2932
30- // DefaultClientOptions read from the environment (ANTHROPIC_API_KEY,
31- // ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL). This should be used to initialize new
32- // clients.
33+ // DefaultClientOptions walks the default credential chain per the
34+ // cross-SDK credential precedence spec:
35+ //
36+ // 1. ANTHROPIC_API_KEY
37+ // 2. ANTHROPIC_AUTH_TOKEN
38+ // 3. Explicit profile via ANTHROPIC_PROFILE (surfaces the error if the
39+ // named profile is missing — the user explicitly selected it)
40+ // 4. Env-var federation (ANTHROPIC_FEDERATION_RULE_ID +
41+ // ANTHROPIC_ORGANIZATION_ID + ANTHROPIC_IDENTITY_TOKEN_FILE / _TOKEN)
42+ // 5. Fallback profile (active_config file or literal "default" — a
43+ // quiet miss when absent, so a WIF-configured machine with a
44+ // leftover default profile still uses WIF)
45+ //
46+ // When no source produces a credential, the first request fails with an
47+ // [auth.NoCredentialsError]. If ANTHROPIC_PROFILE points at a missing or
48+ // invalid profile, the first request instead fails with a wrapped
49+ // profile-load error naming the profile. An explicit credential option
50+ // passed to [NewClient] (e.g. [option.WithAPIKey] or [option.WithAuthToken])
51+ // suppresses both paths. Also honors ANTHROPIC_BASE_URL.
3352func DefaultClientOptions () []option.RequestOption {
3453 defaults := []option.RequestOption {
3554 option .WithHTTPClient (defaultHTTPClient ()),
@@ -38,11 +57,58 @@ func DefaultClientOptions() []option.RequestOption {
3857 if o , ok := os .LookupEnv ("ANTHROPIC_BASE_URL" ); ok {
3958 defaults = append (defaults , option .WithBaseURL (o ))
4059 }
41- if o , ok := os .LookupEnv ("ANTHROPIC_API_KEY" ); ok {
42- defaults = append (defaults , option .WithAPIKey (o ))
60+
61+ statuses := []auth.CredentialSourceStatus {}
62+
63+ if v , ok := os .LookupEnv ("ANTHROPIC_API_KEY" ); ok && v != "" {
64+ defaults = append (defaults , option .WithAPIKey (v ))
65+ return defaults
4366 }
44- if o , ok := os .LookupEnv ("ANTHROPIC_AUTH_TOKEN" ); ok {
45- defaults = append (defaults , option .WithAuthToken (o ))
67+ statuses = append (statuses , auth.CredentialSourceStatus {
68+ Name : "ANTHROPIC_API_KEY env var" ,
69+ State : auth .CredentialSourceNotSet ,
70+ })
71+
72+ if v , ok := os .LookupEnv ("ANTHROPIC_AUTH_TOKEN" ); ok && v != "" {
73+ defaults = append (defaults , option .WithAuthToken (v ))
74+ return defaults
75+ }
76+ statuses = append (statuses , auth.CredentialSourceStatus {
77+ Name : "ANTHROPIC_AUTH_TOKEN env var" ,
78+ State : auth .CredentialSourceNotSet ,
79+ })
80+
81+ // Step 3: explicit profile via ANTHROPIC_PROFILE. The user named a
82+ // specific profile, so a load failure is surfaced immediately — do
83+ // not fall through to env federation or the fallback profile.
84+ if profile , ok := os .LookupEnv ("ANTHROPIC_PROFILE" ); ok && profile != "" {
85+ cfg , err := config .LoadProfile (config .DefaultDir (), profile )
86+ if err != nil {
87+ return append (defaults , explicitProfileErrorOption (profile , err ))
88+ }
89+ return append (defaults , option .WithConfig (cfg ))
90+ }
91+
92+ // Step 4: env-var federation. Beats the fallback profile so a
93+ // WIF-configured machine with a leftover default profile file still
94+ // uses WIF.
95+ envResult , envDetail , envState := auth .EnvCredentials ()
96+ if envResult != nil {
97+ defaults = append (defaults , auth .WithAuthMiddleware (envResult .Provider ))
98+ return defaults
99+ }
100+ envFederationStatus := auth.CredentialSourceStatus {
101+ Name : "env federation (ANTHROPIC_FEDERATION_RULE_ID + ANTHROPIC_ORGANIZATION_ID + ANTHROPIC_IDENTITY_TOKEN_FILE)" ,
102+ State : envState ,
103+ Detail : envDetail ,
104+ }
105+
106+ // Step 5: fallback profile (active_config or literal "default"). A
107+ // missing profile here is a quiet miss — fall through to the no-
108+ // credentials aggregate.
109+ fallbackStatus , fallbackOpt := tryLoadFallbackProfile ()
110+ if fallbackOpt != nil {
111+ return append (defaults , fallbackOpt )
46112 }
47113 if o , ok := os .LookupEnv ("ANTHROPIC_CUSTOM_HEADERS" ); ok {
48114 for _ , line := range strings .Split (o , "\n " ) {
@@ -52,15 +118,96 @@ func DefaultClientOptions() []option.RequestOption {
52118 }
53119 }
54120 }
121+
122+ statuses = append (statuses , envFederationStatus , fallbackStatus )
123+ defaults = append (defaults , noCredentialsSentinel (statuses ))
55124 return defaults
56125}
57126
127+ // tryLoadFallbackProfile attempts the step-5 fallback profile lookup:
128+ // active_config file, otherwise literal "default". A missing profile is
129+ // reported as a silent-miss status (the caller will fall through to the
130+ // no-credentials aggregate); any other load error is reported as a
131+ // load-failure status so the user sees the specific OS error.
132+ func tryLoadFallbackProfile () (auth.CredentialSourceStatus , option.RequestOption ) {
133+ cfg , err := config .LoadConfig ()
134+ if err != nil {
135+ if errors .Is (err , os .ErrNotExist ) {
136+ return auth.CredentialSourceStatus {
137+ Name : "profile config file" ,
138+ State : auth .CredentialSourceNotFound ,
139+ Detail : "run `anthropic auth login` to create one" ,
140+ }, nil
141+ }
142+ return auth.CredentialSourceStatus {
143+ Name : "profile config file" ,
144+ State : auth .CredentialSourceLoadFailed ,
145+ Detail : err .Error (),
146+ }, nil
147+ }
148+ return auth.CredentialSourceStatus {Name : "profile config file" }, option .WithConfig (cfg )
149+ }
150+
151+ // explicitProfileErrorOption installs a middleware that fails the request
152+ // with the underlying load error, unless a caller-supplied credential
153+ // option preempts the profile. Used when ANTHROPIC_PROFILE names a profile
154+ // whose config file cannot be loaded.
155+ func explicitProfileErrorOption (profile string , loadErr error ) option.RequestOption {
156+ profileErr := fmt .Errorf ("ANTHROPIC_PROFILE=%q: %w" , profile , loadErr )
157+ return requestconfig .RequestOptionFunc (func (r * requestconfig.RequestConfig ) error {
158+ cfg := r
159+ check := func (req * http.Request , next func (* http.Request ) (* http.Response , error )) (* http.Response , error ) {
160+ if cfg .APIKey != "" || cfg .AuthToken != "" {
161+ return next (req )
162+ }
163+ if req .Header .Get ("Authorization" ) != "" || req .Header .Get ("X-Api-Key" ) != "" {
164+ return next (req )
165+ }
166+ return nil , profileErr
167+ }
168+ r .Middlewares = append (r .Middlewares , check )
169+ return nil
170+ })
171+ }
172+
173+ func noCredentialsSentinel (statuses []auth.CredentialSourceStatus ) option.RequestOption {
174+ preBuiltErr := & auth.NoCredentialsError {Sources : statuses }
175+ return requestconfig .RequestOptionFunc (func (r * requestconfig.RequestConfig ) error {
176+ cfg := r
177+ check := func (req * http.Request , next func (* http.Request ) (* http.Response , error )) (* http.Response , error ) {
178+ if cfg .APIKey != "" || cfg .AuthToken != "" {
179+ return next (req )
180+ }
181+ if len (cfg .Middlewares ) > 1 {
182+ return next (req )
183+ }
184+ if req .Header .Get ("Authorization" ) != "" || req .Header .Get ("X-Api-Key" ) != "" {
185+ return next (req )
186+ }
187+ return nil , preBuiltErr
188+ }
189+ r .Middlewares = append (r .Middlewares , check )
190+ return nil
191+ })
192+ }
193+
58194// NewClient generates a new client with the default option read from the
59195// environment (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL). The
60196// option passed in as arguments are applied after these default arguments, and all
61197// option will be passed down to the services and requests that this client makes.
198+ //
199+ // Pass [option.WithoutEnvironmentDefaults] to skip the environment-based
200+ // credential autoload entirely (only the hardcoded production base-URL
201+ // default is kept). Use this when the caller does its own credential
202+ // resolution and wants the SDK to contribute nothing from the environment.
62203func NewClient (opts ... option.RequestOption ) (r Client ) {
63- opts = append (DefaultClientOptions (), opts ... )
204+ var defaults []option.RequestOption
205+ if option .HasWithoutEnvironmentDefaults (opts ) {
206+ defaults = []option.RequestOption {option .WithEnvironmentProduction ()}
207+ } else {
208+ defaults = DefaultClientOptions ()
209+ }
210+ opts = append (defaults , opts ... )
64211
65212 r = Client {Options : opts }
66213
0 commit comments