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 }