88 "encoding/binary"
99 "encoding/hex"
1010 "encoding/json"
11+ "errors"
1112 "io"
13+ "log/slog"
1214 "net/http"
1315 "net/url"
1416 "strings"
@@ -180,12 +182,41 @@ func (s Server) claudeFableBedrockResponse(ctx context.Context, body []byte) (*h
180182 }
181183
182184 if stream && resp .StatusCode == http .StatusOK {
185+ first , peeked , err := peekBedrockStream (resp .Body )
186+ if err != nil || strings .EqualFold (first .messageType , "exception" ) {
187+ if s .Logger != nil {
188+ attrs := []any {"bedrock_source" , sourceName , "region" , region , "saw_message_stop" , false }
189+ if first .exceptionType != "" {
190+ attrs = append (attrs , "exception_type" , first .exceptionType , "message" , bedrockLogPreview (first .payload ))
191+ }
192+ if err != nil {
193+ attrs = append (attrs , "read_err" , err )
194+ }
195+ s .Logger .Warn ("claude-fable bedrock stream failed before first event" , attrs ... )
196+ }
197+ _ = resp .Body .Close ()
198+ body := first .payload
199+ if len (body ) == 0 {
200+ body = []byte (`{"type":"error","error":{"type":"api_error","message":"Bedrock stream failed before first event"}}` )
201+ }
202+ s .recordClaudeFableBedrockCost (started , region , http .StatusServiceUnavailable , bedrockUsage {}, false )
203+ return & http.Response {
204+ Status : "503 Service Unavailable" ,
205+ StatusCode : http .StatusServiceUnavailable ,
206+ Proto : "HTTP/1.1" ,
207+ ProtoMajor : 1 ,
208+ ProtoMinor : 1 ,
209+ Header : http.Header {"Content-Type" : {"application/json" }},
210+ Body : io .NopCloser (bytes .NewReader (body )),
211+ ContentLength : int64 (len (body )),
212+ }, nil
213+ }
183214 pr , pw := io .Pipe ()
184215 go func () {
185- usage , haveUsage := transcodeBedrockToSSE (pw , resp .Body )
216+ result := transcodeBedrockToSSE (pw , io . MultiReader ( bytes . NewReader ( peeked ), resp .Body ), s . Logger , sourceName , region )
186217 _ = resp .Body .Close ()
187218 _ = pw .Close ()
188- s .recordClaudeFableBedrockCost (started , region , http .StatusOK , usage , haveUsage )
219+ s .recordClaudeFableBedrockCost (started , region , http .StatusOK , result . Usage , result . HaveUsage )
189220 }()
190221 return & http.Response {
191222 Status : "200 OK" ,
@@ -401,18 +432,75 @@ func (cfg *BedrockConfig) onThrottle(sourceName, region, model string) {
401432 }
402433}
403434
435+ type bedrockStreamResult struct {
436+ Usage bedrockUsage
437+ HaveUsage bool
438+ SawMessageStop bool
439+ ExceptionType string
440+ ReadErr error
441+ }
442+
443+ type bedrockFrame struct {
444+ payload []byte
445+ messageType string
446+ eventType string
447+ exceptionType string
448+ }
449+
450+ var errBedrockStreamEmpty = errors .New ("bedrock stream ended before first event" )
451+
404452// transcodeBedrockToSSE converts a Bedrock invoke-with-response-stream body (AWS
405453// event-stream framing wrapping Anthropic event JSON) into Anthropic Messages
406454// SSE, which is what a Claude client on the OAuth path expects. It also extracts
407- // token usage for cost tracking. w may be an http.ResponseWriter (flushed per
408- // event) or a plain writer like an io.Pipe (each Write hands the event to the
409- // reader directly).
410- func transcodeBedrockToSSE (w io.Writer , src io.Reader ) ( bedrockUsage , bool ) {
455+ // token usage for cost tracking and reports stream anomalies . w may be an
456+ // http.ResponseWriter (flushed per event) or a plain writer like an io.Pipe
457+ // (each Write hands the event to the reader directly).
458+ func transcodeBedrockToSSE (w io.Writer , src io.Reader , logger * slog. Logger , bedrockSource , region string ) bedrockStreamResult {
411459 flusher , _ := w .(http.Flusher )
412460 var scanner bedrockFrameScanner
413- var usage bedrockUsage
414- var got bool
415- emit := func (inner []byte ) {
461+ var result bedrockStreamResult
462+ exceptionHandled := false
463+ finalize := func () bedrockStreamResult {
464+ result .HaveUsage = result .HaveUsage && ! result .Usage .empty ()
465+ return result
466+ }
467+ writeErrorEvent := func (errorType , message string ) {
468+ payload , _ := json .Marshal (map [string ]any {
469+ "type" : "error" ,
470+ "error" : map [string ]string {
471+ "type" : errorType ,
472+ "message" : message ,
473+ },
474+ })
475+ _ , _ = w .Write ([]byte ("event: error\n data: " ))
476+ _ , _ = w .Write (payload )
477+ _ , _ = w .Write ([]byte ("\n \n " ))
478+ if flusher != nil {
479+ flusher .Flush ()
480+ }
481+ }
482+ emit := func (frame bedrockFrame ) {
483+ if exceptionHandled {
484+ return
485+ }
486+ if strings .EqualFold (frame .messageType , "exception" ) {
487+ result .ExceptionType = frame .exceptionType
488+ exceptionHandled = true
489+ if logger != nil {
490+ logger .Warn ("claude-fable bedrock mid-stream exception" , "exception_type" , frame .exceptionType , "message" , bedrockLogPreview (frame .payload ), "bedrock_source" , bedrockSource , "region" , region , "saw_message_stop" , result .SawMessageStop )
491+ }
492+ if strings .Contains (strings .ToLower (frame .exceptionType ), "throttl" ) {
493+ writeErrorEvent ("overloaded_error" , "Bedrock throttled mid-stream" )
494+ } else {
495+ message := "Bedrock stream error"
496+ if frame .exceptionType != "" {
497+ message += ": " + frame .exceptionType
498+ }
499+ writeErrorEvent ("api_error" , message )
500+ }
501+ return
502+ }
503+ inner := frame .payload
416504 var ev struct {
417505 Type string `json:"type"`
418506 Message struct {
@@ -423,14 +511,20 @@ func transcodeBedrockToSSE(w io.Writer, src io.Reader) (bedrockUsage, bool) {
423511 _ = json .Unmarshal (inner , & ev )
424512 switch ev .Type {
425513 case "message_start" :
426- usage .InputTokens = ev .Message .Usage .InputTokens
427- usage .CacheReadTokens = ev .Message .Usage .CacheReadTokens
428- usage .CacheWriteTokens = ev .Message .Usage .CacheWriteTokens
429- got = true
514+ result . Usage .InputTokens = ev .Message .Usage .InputTokens
515+ result . Usage .CacheReadTokens = ev .Message .Usage .CacheReadTokens
516+ result . Usage .CacheWriteTokens = ev .Message .Usage .CacheWriteTokens
517+ result . HaveUsage = true
430518 case "message_delta" :
431519 if ev .Usage .OutputTokens > 0 {
432- usage .OutputTokens = ev .Usage .OutputTokens
433- got = true
520+ result .Usage .OutputTokens = ev .Usage .OutputTokens
521+ result .HaveUsage = true
522+ }
523+ case "message_stop" :
524+ result .SawMessageStop = true
525+ case "error" :
526+ if logger != nil {
527+ logger .Warn ("claude-fable bedrock in-band error" , "exception_type" , "" , "message" , bedrockLogPreview (inner ), "bedrock_source" , bedrockSource , "region" , region , "saw_message_stop" , result .SawMessageStop )
434528 }
435529 }
436530 eventType := ev .Type
@@ -449,19 +543,33 @@ func transcodeBedrockToSSE(w io.Writer, src io.Reader) (bedrockUsage, bool) {
449543 n , readErr := src .Read (buf )
450544 if n > 0 {
451545 scanner .feed (buf [:n ], emit )
546+ if exceptionHandled {
547+ return finalize ()
548+ }
452549 }
453550 if readErr != nil {
551+ if ! result .SawMessageStop && ! exceptionHandled {
552+ result .ReadErr = readErr
553+ if logger != nil {
554+ logger .Error ("claude-fable bedrock stream truncated" , "bedrock_source" , bedrockSource , "region" , region , "read_err" , readErr , "saw_message_stop" , result .SawMessageStop , "exception_type" , "" , "message" , "" )
555+ }
556+ writeErrorEvent ("api_error" , "Bedrock stream interrupted" )
557+ } else if readErr != io .EOF {
558+ result .ReadErr = readErr
559+ }
454560 break
455561 }
456562 }
457- return usage , got && ! usage . empty ()
563+ return finalize ()
458564}
459565
460566// bedrockFrameScanner parses AWS event-stream frames incrementally and yields
461- // each frame's decoded inner payload (the Anthropic event JSON).
567+ // each frame's decoded inner payload (the Anthropic event JSON) with AWS
568+ // event-stream metadata. Exception frames carry a raw JSON error payload instead
569+ // of a {"bytes": "..."} wrapper.
462570type bedrockFrameScanner struct { buf []byte }
463571
464- func (s * bedrockFrameScanner ) feed (data []byte , emit func ([] byte )) {
572+ func (s * bedrockFrameScanner ) feed (data []byte , emit func (bedrockFrame )) {
465573 s .buf = append (s .buf , data ... )
466574 for {
467575 if len (s .buf ) < 12 {
@@ -478,20 +586,139 @@ func (s *bedrockFrameScanner) feed(data []byte, emit func([]byte)) {
478586 }
479587 payloadStart := 12 + headersLen
480588 payloadEnd := total - 4
589+ headers := map [string ]string (nil )
590+ if headersLen >= 0 && 12 + headersLen <= payloadEnd {
591+ headers , _ = parseBedrockEventHeaders (s .buf [12 : 12 + headersLen ])
592+ } else {
593+ payloadStart = 12
594+ }
481595 if payloadStart <= payloadEnd && payloadEnd <= len (s .buf ) {
596+ frame := bedrockFrame {
597+ messageType : headers [":message-type" ],
598+ eventType : headers [":event-type" ],
599+ exceptionType : headers [":exception-type" ],
600+ }
601+ payload := s .buf [payloadStart :payloadEnd ]
602+ if strings .EqualFold (frame .messageType , "exception" ) {
603+ frame .payload = append ([]byte (nil ), payload ... )
604+ emit (frame )
605+ s .buf = s .buf [total :]
606+ continue
607+ }
482608 var wrap struct {
483609 Bytes string `json:"bytes"`
484610 }
485- if json .Unmarshal (s . buf [ payloadStart : payloadEnd ] , & wrap ) == nil && wrap .Bytes != "" {
611+ if json .Unmarshal (payload , & wrap ) == nil && wrap .Bytes != "" {
486612 if decoded , err := base64 .StdEncoding .DecodeString (wrap .Bytes ); err == nil {
487- emit (decoded )
613+ frame .payload = decoded
614+ emit (frame )
488615 }
489616 }
490617 }
491618 s .buf = s .buf [total :]
492619 }
493620}
494621
622+ func parseBedrockEventHeaders (headers []byte ) (map [string ]string , bool ) {
623+ out := make (map [string ]string )
624+ for len (headers ) > 0 {
625+ if len (headers ) < 2 {
626+ return nil , false
627+ }
628+ nameLen := int (headers [0 ])
629+ headers = headers [1 :]
630+ if len (headers ) < nameLen + 1 {
631+ return nil , false
632+ }
633+ name := string (headers [:nameLen ])
634+ headers = headers [nameLen :]
635+ valueType := headers [0 ]
636+ headers = headers [1 :]
637+ switch valueType {
638+ case 0 , 1 :
639+ case 2 :
640+ if len (headers ) < 1 {
641+ return nil , false
642+ }
643+ headers = headers [1 :]
644+ case 3 :
645+ if len (headers ) < 2 {
646+ return nil , false
647+ }
648+ headers = headers [2 :]
649+ case 4 :
650+ if len (headers ) < 4 {
651+ return nil , false
652+ }
653+ headers = headers [4 :]
654+ case 5 , 8 :
655+ if len (headers ) < 8 {
656+ return nil , false
657+ }
658+ headers = headers [8 :]
659+ case 6 , 7 :
660+ if len (headers ) < 2 {
661+ return nil , false
662+ }
663+ valueLen := int (binary .BigEndian .Uint16 (headers [:2 ]))
664+ headers = headers [2 :]
665+ if len (headers ) < valueLen {
666+ return nil , false
667+ }
668+ if valueType == 7 {
669+ out [name ] = string (headers [:valueLen ])
670+ }
671+ headers = headers [valueLen :]
672+ case 9 :
673+ if len (headers ) < 16 {
674+ return nil , false
675+ }
676+ headers = headers [16 :]
677+ default :
678+ return nil , false
679+ }
680+ }
681+ return out , true
682+ }
683+
684+ func peekBedrockStream (src io.Reader ) (bedrockFrame , []byte , error ) {
685+ var scanner bedrockFrameScanner
686+ var first bedrockFrame
687+ var got bool
688+ var peeked bytes.Buffer
689+ emit := func (frame bedrockFrame ) {
690+ if ! got {
691+ first = frame
692+ got = true
693+ }
694+ }
695+ buf := make ([]byte , 32 * 1024 )
696+ for ! got {
697+ n , readErr := src .Read (buf )
698+ if n > 0 {
699+ peeked .Write (buf [:n ])
700+ scanner .feed (buf [:n ], emit )
701+ }
702+ if got {
703+ break
704+ }
705+ if readErr != nil {
706+ if readErr == io .EOF {
707+ return bedrockFrame {}, peeked .Bytes (), errBedrockStreamEmpty
708+ }
709+ return bedrockFrame {}, peeked .Bytes (), readErr
710+ }
711+ }
712+ return first , peeked .Bytes (), nil
713+ }
714+
715+ func bedrockLogPreview (payload []byte ) string {
716+ if len (payload ) > 256 {
717+ payload = payload [:256 ]
718+ }
719+ return string (payload )
720+ }
721+
495722func (s Server ) handleBedrockCost (w http.ResponseWriter , r * http.Request ) {
496723 path := ""
497724 if s .Bedrock != nil {
0 commit comments