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

     1  package bedrock
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha256"
     7  	"encoding/base64"
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"mime"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"time"
    17  
    18  	"github.com/aws/aws-sdk-go-v2/aws"
    19  	"github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream"
    20  	"github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream/eventstreamapi"
    21  	v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
    22  	"github.com/aws/aws-sdk-go-v2/config"
    23  	"github.com/aws/smithy-go/auth/bearer"
    24  	"github.com/tidwall/gjson"
    25  	"github.com/tidwall/sjson"
    26  
    27  	"github.com/anthropics/anthropic-sdk-go/internal/requestconfig"
    28  	"github.com/anthropics/anthropic-sdk-go/option"
    29  )
    30  
    31  const DefaultVersion = "bedrock-2023-05-31"
    32  
    33  var DefaultEndpoints = map[string]bool{
    34  	"/v1/complete": true,
    35  	"/v1/messages": true,
    36  }
    37  
    38  func NewStaticBearerTokenProvider(token string) *bearer.StaticTokenProvider {
    39  	return &bearer.StaticTokenProvider{
    40  		Token: bearer.Token{
    41  			Value:     token,
    42  			CanExpire: false,
    43  		},
    44  	}
    45  }
    46  
    47  type eventstreamChunk struct {
    48  	Bytes string `json:"bytes"`
    49  	P     string `json:"p"`
    50  }
    51  
    52  // sseTranslatingBody converts an AWS binary EventStream response body into SSE
    53  // wire format (text/event-stream), so that middleware outside the Bedrock
    54  // adaptation and stream consumers observe the same response shape on Bedrock
    55  // as on the first-party API.
    56  type sseTranslatingBody struct {
    57  	eventstream.Decoder
    58  
    59  	rc  io.ReadCloser
    60  	buf bytes.Buffer
    61  	err error
    62  }
    63  
    64  func (b *sseTranslatingBody) Read(p []byte) (int, error) {
    65  	// Buffered SSE bytes must drain before a translation error surfaces, so
    66  	// events decoded ahead of a mid-stream exception still reach the consumer.
    67  	for b.buf.Len() == 0 {
    68  		if b.err != nil {
    69  			return 0, b.err
    70  		}
    71  
    72  		msg, err := b.Decoder.Decode(b.rc, nil)
    73  		if err != nil {
    74  			b.err = err
    75  			continue
    76  		}
    77  		b.translate(msg)
    78  	}
    79  	return b.buf.Read(p)
    80  }
    81  
    82  func (b *sseTranslatingBody) Close() error {
    83  	return b.rc.Close()
    84  }
    85  
    86  func (b *sseTranslatingBody) translate(msg eventstream.Message) {
    87  	messageType := msg.Headers.Get(eventstreamapi.MessageTypeHeader)
    88  	if messageType == nil {
    89  		b.err = fmt.Errorf("%s event header not present", eventstreamapi.MessageTypeHeader)
    90  		return
    91  	}
    92  
    93  	switch messageType.String() {
    94  	case eventstreamapi.EventMessageType:
    95  		eventType := msg.Headers.Get(eventstreamapi.EventTypeHeader)
    96  		if eventType == nil {
    97  			b.err = fmt.Errorf("%s event header not present", eventstreamapi.EventTypeHeader)
    98  			return
    99  		}
   100  
   101  		if eventType.String() == "chunk" {
   102  			chunk := eventstreamChunk{}
   103  			err := json.Unmarshal(msg.Payload, &chunk)
   104  			if err != nil {
   105  				b.err = err
   106  				return
   107  			}
   108  			decoded, err := base64.StdEncoding.DecodeString(chunk.Bytes)
   109  			if err != nil {
   110  				b.err = err
   111  				return
   112  			}
   113  			b.emit(gjson.GetBytes(decoded, "type").String(), decoded)
   114  		}
   115  
   116  	case eventstreamapi.ExceptionMessageType:
   117  		// See https://github.com/aws/aws-sdk-go-v2/blob/885de40869f9bcee29ad11d60967aa0f1b571d46/service/iotsitewise/deserializers.go#L15511C1-L15567C2
   118  		exceptionType := msg.Headers.Get(eventstreamapi.ExceptionTypeHeader)
   119  		if exceptionType == nil {
   120  			b.err = fmt.Errorf("%s event header not present", eventstreamapi.ExceptionTypeHeader)
   121  			return
   122  		}
   123  
   124  		// See https://github.com/aws/aws-sdk-go-v2/blob/885de40869f9bcee29ad11d60967aa0f1b571d46/aws/protocol/restjson/decoder_util.go#L15-L48k
   125  		var errInfo struct {
   126  			Code    string
   127  			Type    string `json:"__type"`
   128  			Message string
   129  		}
   130  		err := json.Unmarshal(msg.Payload, &errInfo)
   131  		if err != nil && err != io.EOF {
   132  			b.err = fmt.Errorf("received exception %s: parsing exception payload failed: %w", exceptionType.String(), err)
   133  			return
   134  		}
   135  
   136  		errorCode := "UnknownError"
   137  		errorMessage := errorCode
   138  		if ev := exceptionType.String(); len(ev) > 0 {
   139  			errorCode = ev
   140  		} else if len(errInfo.Code) > 0 {
   141  			errorCode = errInfo.Code
   142  		} else if len(errInfo.Type) > 0 {
   143  			errorCode = errInfo.Type
   144  		}
   145  
   146  		if len(errInfo.Message) > 0 {
   147  			errorMessage = errInfo.Message
   148  		}
   149  		b.err = fmt.Errorf("received exception %s: %s", errorCode, errorMessage)
   150  
   151  	case eventstreamapi.ErrorMessageType:
   152  		errorCode := "UnknownError"
   153  		errorMessage := errorCode
   154  		if header := msg.Headers.Get(eventstreamapi.ErrorCodeHeader); header != nil {
   155  			errorCode = header.String()
   156  		}
   157  		if header := msg.Headers.Get(eventstreamapi.ErrorMessageHeader); header != nil {
   158  			errorMessage = header.String()
   159  		}
   160  		b.err = fmt.Errorf("received error %s: %s", errorCode, errorMessage)
   161  	}
   162  }
   163  
   164  func (b *sseTranslatingBody) emit(eventType string, data []byte) {
   165  	b.buf.WriteString("event: ")
   166  	b.buf.WriteString(eventType)
   167  	b.buf.WriteByte('\n')
   168  	// The SSE format carries one "data:" line per payload line; consumers
   169  	// rejoin multi-line data with '\n'. API event JSON contains no raw
   170  	// newlines, but split defensively to keep the framing valid either way.
   171  	for _, line := range bytes.Split(data, []byte("\n")) {
   172  		b.buf.WriteString("data: ")
   173  		b.buf.Write(line)
   174  		b.buf.WriteByte('\n')
   175  	}
   176  	b.buf.WriteByte('\n')
   177  }
   178  
   179  // WithLoadDefaultConfig returns a request option which loads the default config for Amazon and registers
   180  // middleware that intercepts request to the Messages API so that this SDK can be used with Amazon Bedrock.
   181  //
   182  // If you already have an [aws.Config], it is recommended that you instead call [WithConfig] directly.
   183  //
   184  // Register any [option.WithMiddleware] before this option so your middleware
   185  // observes Anthropic-shaped requests and responses; see [WithConfig].
   186  //
   187  // Like [WithConfig], the returned option includes
   188  // [option.WithoutEnvironmentDefaults], so first-party credential sources
   189  // never apply to a client constructed with it.
   190  func WithLoadDefaultConfig(ctx context.Context, optFns ...func(*config.LoadOptions) error) option.RequestOption {
   191  	cfg, err := config.LoadDefaultConfig(ctx, optFns...)
   192  	if err != nil {
   193  		panic(err)
   194  	}
   195  	return WithConfig(cfg)
   196  }
   197  
   198  // WithConfig returns a request option that uses the provided config and registers middleware to
   199  // intercept requests to the Messages API, enabling this SDK to work with Amazon Bedrock.
   200  //
   201  // Authentication is resolved once, in this order:
   202  //  1. If the AWS_BEARER_TOKEN_BEDROCK environment variable is set, its value is sent as a bearer token.
   203  //  2. If cfg.AuthSchemePreference lists "httpBearerAuth" first, cfg.BearerAuthTokenProvider is used.
   204  //  3. If cfg.Credentials is set, requests are signed with AWS SigV4.
   205  //  4. Otherwise cfg.BearerAuthTokenProvider is used, and one of the two must be set.
   206  //
   207  // [config.LoadDefaultConfig] sets cfg.BearerAuthTokenProvider from the SSO token cache for SSO
   208  // profiles; step 3 keeps that token from being sent to Bedrock.
   209  //
   210  // The Bedrock adaptation (URL and body rewriting, request signing, and
   211  // normalization of streaming responses to SSE) should run closest to the wire.
   212  // Middleware runs in registration order, so register [option.WithMiddleware]
   213  // before this option:
   214  //
   215  //	client := anthropic.NewClient(
   216  //		option.WithMiddleware(loggingMiddleware),
   217  //		bedrock.WithConfig(cfg),
   218  //	)
   219  //
   220  // Ordered this way, your middleware observes Anthropic-shaped requests
   221  // (POST /v1/messages with model and stream in the body, no AWS signature)
   222  // and SSE-formatted streaming responses — identical to the first-party API.
   223  // Note that mutating the request after the Bedrock middleware has signed it
   224  // invalidates the SigV4 signature, so body- or header-mutating middleware
   225  // must be registered before this option.
   226  //
   227  // The returned option is an [option.Join] of [option.WithoutEnvironmentDefaults]
   228  // and the Bedrock configuration, so a client constructed with it skips the
   229  // SDK's first-party credential autoload entirely: environment credentials
   230  // and profiles from the shared config store (ANTHROPIC_API_KEY,
   231  // ANTHROPIC_PROFILE, ANTHROPIC_CONFIG_DIR) never apply to Bedrock requests.
   232  func WithConfig(cfg aws.Config) option.RequestOption {
   233  	var credentialErr error
   234  
   235  	cfg = resolveAuth(cfg)
   236  	if cfg.BearerAuthTokenProvider == nil && cfg.Credentials == nil {
   237  		credentialErr = fmt.Errorf("expected AWS credentials to be set")
   238  	}
   239  
   240  	signer := v4.NewSigner()
   241  	middleware := bedrockMiddleware(signer, cfg)
   242  
   243  	return option.Join(option.WithoutEnvironmentDefaults(), requestconfig.RequestOptionFunc(func(rc *requestconfig.RequestConfig) error {
   244  		if credentialErr != nil {
   245  			return credentialErr
   246  		}
   247  		return rc.Apply(
   248  			option.WithBaseURL(fmt.Sprintf("https://bedrock-runtime.%s.amazonaws.com", cfg.Region)),
   249  			option.WithMiddleware(middleware),
   250  		)
   251  	}))
   252  }
   253  
   254  // resolveAuth returns cfg with BearerAuthTokenProvider set only when requests
   255  // should carry a bearer token; a nil provider means SigV4 with cfg.Credentials.
   256  func resolveAuth(cfg aws.Config) aws.Config {
   257  	prefersBearer := len(cfg.AuthSchemePreference) > 0 && cfg.AuthSchemePreference[0] == "httpBearerAuth"
   258  
   259  	token := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
   260  
   261  	switch {
   262  	case token != "":
   263  		cfg.BearerAuthTokenProvider = NewStaticBearerTokenProvider(token)
   264  	case prefersBearer && cfg.BearerAuthTokenProvider != nil:
   265  	case cfg.Credentials != nil:
   266  		cfg.BearerAuthTokenProvider = nil
   267  	}
   268  	return cfg
   269  }
   270  
   271  func bedrockMiddleware(signer *v4.Signer, cfg aws.Config) option.Middleware {
   272  	return func(r *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
   273  		var body []byte
   274  		if r.Body != nil {
   275  			body, err = io.ReadAll(r.Body)
   276  			if err != nil {
   277  				return nil, err
   278  			}
   279  			r.Body.Close()
   280  
   281  			// Raw (non-JSON) bodies must reach the wire byte-for-byte; only a
   282  			// JSON body carries fields to inject or strip.
   283  			isJSON := gjson.ValidBytes(body)
   284  
   285  			if isJSON && !gjson.GetBytes(body, "anthropic_version").Exists() {
   286  				body, _ = sjson.SetBytes(body, "anthropic_version", DefaultVersion)
   287  			}
   288  
   289  			// pull the betas off of the header (if set) and put them in the body
   290  			if betaHeader := r.Header.Values("anthropic-beta"); isJSON && len(betaHeader) > 0 {
   291  				r.Header.Del("anthropic-beta")
   292  				body, err = sjson.SetBytes(body, "anthropic_beta", betaHeader)
   293  				if err != nil {
   294  					return nil, err
   295  				}
   296  			}
   297  
   298  			if isJSON && r.Method == http.MethodPost && DefaultEndpoints[r.URL.Path] {
   299  				model := gjson.GetBytes(body, "model").String()
   300  				stream := gjson.GetBytes(body, "stream").Bool()
   301  
   302  				body, _ = sjson.DeleteBytes(body, "model")
   303  				body, _ = sjson.DeleteBytes(body, "stream")
   304  
   305  				var method string
   306  				if stream {
   307  					method = "invoke-with-response-stream"
   308  				} else {
   309  					method = "invoke"
   310  				}
   311  
   312  				r.URL.Path = fmt.Sprintf("/model/%s/%s", model, method)
   313  				r.URL.RawPath = fmt.Sprintf("/model/%s/%s", url.QueryEscape(model), method)
   314  			}
   315  
   316  			reader := bytes.NewReader(body)
   317  			r.Body = io.NopCloser(reader)
   318  			r.GetBody = func() (io.ReadCloser, error) {
   319  				_, err := reader.Seek(0, 0)
   320  				return io.NopCloser(reader), err
   321  			}
   322  			r.ContentLength = int64(len(body))
   323  		}
   324  
   325  		// WithConfig resolves the auth mode: a provider here means bearer.
   326  		if cfg.BearerAuthTokenProvider != nil {
   327  			token, err := cfg.BearerAuthTokenProvider.RetrieveBearerToken(r.Context())
   328  			if err != nil {
   329  				return nil, err
   330  			}
   331  			r.Header.Set("Authorization", "Bearer "+token.Value)
   332  		} else {
   333  			ctx := r.Context()
   334  			credentials, err := cfg.Credentials.Retrieve(ctx)
   335  			if err != nil {
   336  				return nil, err
   337  			}
   338  
   339  			hash := sha256.Sum256(body)
   340  			err = signer.SignHTTP(ctx, credentials, r, hex.EncodeToString(hash[:]), "bedrock", cfg.Region, time.Now())
   341  			if err != nil {
   342  				return nil, err
   343  			}
   344  		}
   345  
   346  		res, err = next(r)
   347  		if err != nil || res == nil {
   348  			return res, err
   349  		}
   350  
   351  		// Normalize streaming responses to the SSE format the first-party API
   352  		// uses, so layers above this middleware never see AWS EventStream.
   353  		// Error responses stay untranslated: the SDK's error path reads the
   354  		// raw body to build an error carrying the status and request ID.
   355  		if mediaType, _, _ := mime.ParseMediaType(res.Header.Get("Content-Type")); res.StatusCode < 400 && mediaType == "application/vnd.amazon.eventstream" {
   356  			res.Body = &sseTranslatingBody{rc: res.Body}
   357  			res.Header.Set("Content-Type", "text/event-stream")
   358  			res.Header.Del("Content-Length")
   359  			res.ContentLength = -1
   360  		}
   361  
   362  		return res, nil
   363  	}
   364  }