github.com/anthropics/anthropic-sdk-go@v1.70.1/googlecloud/auth.go (about) 1 // Config resolution and bearer-token auth for the Anthropic Google Cloud client. 2 3 package googlecloud 4 5 import ( 6 "context" 7 "fmt" 8 "net/http" 9 "os" 10 11 "golang.org/x/oauth2" 12 "golang.org/x/oauth2/google" 13 14 "github.com/anthropics/anthropic-sdk-go/option" 15 ) 16 17 // Environment variables consulted as fallbacks for the corresponding 18 // [ClientConfig] fields. 19 const ( 20 envProject = "ANTHROPIC_GOOGLE_CLOUD_PROJECT" 21 envLocation = "ANTHROPIC_GOOGLE_CLOUD_LOCATION" 22 envWorkspaceID = "ANTHROPIC_GOOGLE_CLOUD_WORKSPACE_ID" 23 envBaseURL = "ANTHROPIC_GOOGLE_CLOUD_BASE_URL" 24 ) 25 26 // cloudPlatformScope is the OAuth2 scope required to call the gateway. 27 const cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" 28 29 // baseURLTemplate is the gateway base URL; override via BaseURL if needed. 30 // %[1]s is the project, %[2]s the location, %[3]s the workspace ID. 31 const baseURLTemplate = "https://claude.googleapis.com/v1alpha/projects/%[1]s/locations/%[2]s/workspaces/%[3]s/invoke" 32 33 // defaultLocation is the location used when none is configured; the gateway 34 // should always be addressed via the global region. 35 const defaultLocation = "global" 36 37 // resolveConfig applies environment-variable fallbacks and derives the base URL 38 // when a project is known. It does not touch the network; ADC-based project 39 // backfill happens in createClientOptions. Location defaults to "global" when 40 // not configured. The workspace ID is embedded in the derived base URL, so it 41 // is required unless SkipAuth is set together with an explicit BaseURL. 42 func resolveConfig(cfg ClientConfig) (ClientConfig, error) { 43 cfg.Project = firstNonEmpty(cfg.Project, os.Getenv(envProject), os.Getenv("GOOGLE_CLOUD_PROJECT")) 44 cfg.Location = firstNonEmpty(cfg.Location, os.Getenv(envLocation), defaultLocation) 45 cfg.BaseURL = firstNonEmpty(cfg.BaseURL, os.Getenv(envBaseURL)) 46 cfg.WorkspaceID = firstNonEmpty(cfg.WorkspaceID, os.Getenv(envWorkspaceID)) 47 48 if cfg.SkipAuth && cfg.TokenSource != nil { 49 return cfg, fmt.Errorf("googlecloud: SkipAuth and TokenSource are mutually exclusive; set one or the other") 50 } 51 52 // The workspace ID is required unless SkipAuth is set together with an 53 // explicit BaseURL (no URL to derive). 54 if cfg.WorkspaceID == "" && !(cfg.SkipAuth && cfg.BaseURL != "") { 55 return cfg, fmt.Errorf("googlecloud: no workspace ID found; set WorkspaceID or the %s environment variable", envWorkspaceID) 56 } 57 58 if cfg.BaseURL == "" && cfg.Project != "" { 59 cfg.BaseURL = deriveBaseURL(cfg.Project, cfg.Location, cfg.WorkspaceID) 60 } 61 62 return cfg, nil 63 } 64 65 // createClientOptions resolves configuration and returns the request options that 66 // configure an Anthropic client for the Google Cloud gateway: the base URL 67 // (which embeds the workspace ID) and a Google bearer-token middleware. 68 func createClientOptions(ctx context.Context, cfg ClientConfig) ([]option.RequestOption, error) { 69 resolved, err := resolveConfig(cfg) 70 if err != nil { 71 return nil, err 72 } 73 74 // Resolve credentials first: ADC also yields a project we can use to backfill 75 // both an unset Project and the derived base URL. 76 var ts oauth2.TokenSource 77 if !resolved.SkipAuth { 78 ts = resolved.TokenSource 79 if ts == nil { 80 creds, err := google.FindDefaultCredentials(ctx, cloudPlatformScope) 81 if err != nil { 82 return nil, fmt.Errorf("googlecloud: failed to load Google application default credentials: %w", err) 83 } 84 ts = creds.TokenSource 85 if resolved.Project == "" { 86 resolved.Project = creds.ProjectID 87 } 88 } 89 // Guarantee the per-request caching the middleware relies on, regardless of 90 // what the caller passed (ADC's source already caches; a naive caller source 91 // would otherwise be hit on every request and retry). 92 ts = oauth2.ReuseTokenSource(nil, ts) 93 } 94 95 if resolved.BaseURL == "" { 96 if resolved.Project == "" { 97 return nil, fmt.Errorf("googlecloud: no project found; set Project, set the %s environment variable, or configure application default credentials with a project", envProject) 98 } 99 resolved.BaseURL = deriveBaseURL(resolved.Project, resolved.Location, resolved.WorkspaceID) 100 } 101 102 opts := []option.RequestOption{option.WithBaseURL(resolved.BaseURL)} 103 104 if resolved.SkipAuth { 105 return opts, nil 106 } 107 108 opts = append(opts, option.WithMiddleware(bearerMiddleware(ts))) 109 return opts, nil 110 } 111 112 // bearerMiddleware attaches a Google OAuth2 access token to each request as 113 // Authorization: Bearer, only if the header is not already set. The token source 114 // caches and refreshes internally, so this is cheap per request. Unlike SigV4, a 115 // bearer token does not depend on the request body, so the body is never read or 116 // buffered here — streaming and multipart uploads pass through untouched. 117 func bearerMiddleware(ts oauth2.TokenSource) option.Middleware { 118 return func(r *http.Request, next option.MiddlewareNext) (*http.Response, error) { 119 if r.Header.Get("Authorization") == "" { 120 tok, err := ts.Token() 121 if err != nil { 122 return nil, fmt.Errorf("googlecloud: failed to fetch Google access token: %w", err) 123 } 124 tok.SetAuthHeader(r) 125 } 126 return next(r) 127 } 128 } 129 130 func deriveBaseURL(project, location, workspaceID string) string { 131 return fmt.Sprintf(baseURLTemplate, project, location, workspaceID) 132 } 133 134 func firstNonEmpty(vals ...string) string { 135 for _, v := range vals { 136 if v != "" { 137 return v 138 } 139 } 140 return "" 141 }