github.com/anthropics/anthropic-sdk-go@v1.70.1/lib/betafallback/streaming.go (about)

     1  package betafallback
     2  
     3  // Streaming half of [BetaRefusalFallbackMiddleware]: a refused streaming
     4  // attempt is retried down the chain and the retry's events are spliced onto
     5  // the open stream in server-parity framing — one message_start, a fallback
     6  // boundary block at each model seam, monotonic block indices, and a terminal
     7  // usage.iterations ledger covering every hop. The splicer is pull-based: hop
     8  // requests are issued from Read on the consumer's goroutine.
     9  //
    10  // The streaming contract differs from the non-streaming loop where the wire
    11  // does:
    12  //   - A mid-stream refusal retries only when it minted a credit token; with
    13  //     content already streamed and no token, the refusal surfaces untouched.
    14  //     A pre-stream refusal retries either way — nothing streamed, so the
    15  //     retry is invisible.
    16  //   - When the refusal advertises stop_details.fallback_has_prefill_claim,
    17  //     the retry appends one trailing assistant turn echoing the refused
    18  //     partial verbatim; the continuation grows across hops.
    19  //   - A hop whose request fails (HTTP error, transport error, or a non-SSE
    20  //     200) was never reached: the token and continuation carry to the next
    21  //     entry and no boundary is emitted for it.
    22  //   - The non-streaming loop does not synthesize usage.iterations; only the
    23  //     fallback boundary blocks are prepended.
    24  //   - The non-streaming loop drops a refused hop's partial output; only the
    25  //     serving hop's content reaches the caller.
    26  
    27  import (
    28  	"bytes"
    29  	"encoding/json"
    30  	"fmt"
    31  	"io"
    32  	"mime"
    33  	"net/http"
    34  	"sort"
    35  	"strings"
    36  
    37  	"github.com/tidwall/gjson"
    38  	"github.com/tidwall/sjson"
    39  
    40  	"github.com/anthropics/anthropic-sdk-go"
    41  	"github.com/anthropics/anthropic-sdk-go/packages/ssestream"
    42  )
    43  
    44  // handleStreaming runs the streaming fallback path for an eligible request.
    45  // body is the parsed original request body (stream: true).
    46  func handleStreaming(
    47  	req *http.Request,
    48  	next func(*http.Request) (*http.Response, error),
    49  	orig []byte,
    50  	body map[string]json.RawMessage,
    51  	fallbacks []anthropic.BetaFallbackParam,
    52  	state *BetaFallbackState,
    53  ) (*http.Response, error) {
    54  	send := func(payload []byte) (*http.Response, error) {
    55  		r := requestWithBody(req, payload)
    56  		ensureFallbackCreditBeta(r.Header)
    57  		return next(r)
    58  	}
    59  
    60  	orig = trimHistory(body, orig)
    61  
    62  	chainStart := 0
    63  	attempt := orig
    64  	if state != nil && state.Index() >= 0 {
    65  		var err error
    66  		if attempt, _, err = mergeFallback(body, fallbacks[state.Index()], ""); err != nil {
    67  			return nil, err
    68  		}
    69  		chainStart = state.Index() + 1
    70  	}
    71  
    72  	res, err := send(attempt)
    73  	if err != nil || !isEventStream(res) {
    74  		// Errors and non-SSE responses surface verbatim; there is no refusal
    75  		// to act on.
    76  		return res, err
    77  	}
    78  
    79  	s := &streamSplicer{
    80  		res:       res,
    81  		dec:       ssestream.NewDecoder(res),
    82  		body:      body,
    83  		fallbacks: fallbacks,
    84  		next:      chainStart,
    85  		state:     state,
    86  		send:      send,
    87  		hopModel:  gjson.GetBytes(attempt, "model").String(),
    88  		open:      map[int]bool{},
    89  		blocks:    map[int]*rawBlock{},
    90  	}
    91  	s.effModel = s.hopModel
    92  
    93  	out := new(http.Response)
    94  	*out = *res
    95  	out.Body = s
    96  	out.ContentLength = -1
    97  	out.Header = res.Header.Clone()
    98  	out.Header.Del("Content-Length")
    99  	return out, nil
   100  }
   101  
   102  // isEventStream reports whether res is a readable SSE stream. An encoded
   103  // body (caller-set Accept-Encoding) can't be inspected.
   104  func isEventStream(res *http.Response) bool {
   105  	if res == nil || res.StatusCode != http.StatusOK || res.Header.Get("Content-Encoding") != "" {
   106  		return false
   107  	}
   108  	mediaType, _, _ := mime.ParseMediaType(res.Header.Get("Content-Type"))
   109  	return mediaType == "text/event-stream"
   110  }
   111  
   112  // rawBlock accumulates one content block of the current hop from its start
   113  // event and deltas, so a prefill claim can echo it verbatim.
   114  type rawBlock struct {
   115  	start     []byte // raw content_block JSON from content_block_start
   116  	blockType string
   117  	text      strings.Builder
   118  	thinking  strings.Builder
   119  	signature strings.Builder
   120  	partial   strings.Builder // input_json_delta fragments
   121  	citations []json.RawMessage
   122  }
   123  
   124  // finalize folds the accumulated deltas back into the block's JSON.
   125  func (b *rawBlock) finalize() json.RawMessage {
   126  	out := b.start
   127  	set := func(path string, v any) {
   128  		if patched, err := sjson.SetBytes(out, path, v); err == nil {
   129  			out = patched
   130  		}
   131  	}
   132  	if b.text.Len() > 0 {
   133  		set("text", gjson.GetBytes(b.start, "text").String()+b.text.String())
   134  	}
   135  	if b.thinking.Len() > 0 {
   136  		set("thinking", gjson.GetBytes(b.start, "thinking").String()+b.thinking.String())
   137  	}
   138  	if b.signature.Len() > 0 {
   139  		set("signature", gjson.GetBytes(b.start, "signature").String()+b.signature.String())
   140  	}
   141  	if raw := b.partial.String(); raw != "" && gjson.Valid(raw) {
   142  		if patched, err := sjson.SetRawBytes(out, "input", []byte(raw)); err == nil {
   143  			out = patched
   144  		}
   145  	}
   146  	for _, c := range b.citations {
   147  		if patched, err := sjson.SetRawBytes(out, "citations.-1", c); err == nil {
   148  			out = patched
   149  		}
   150  	}
   151  	return out
   152  }
   153  
   154  type seam struct {
   155  	from, to string
   156  	// category is the policy category that triggered the refusal at this
   157  	// boundary; nil when the server didn't surface one. Typed any so nil
   158  	// serializes as JSON null in block().
   159  	category any
   160  }
   161  
   162  // block returns the fallback boundary content block as raw JSON — the shape
   163  // both the streaming splice and the non-streaming prepend emit, and that
   164  // trimFallbackTurns keys off on history replay.
   165  func (s seam) block() json.RawMessage {
   166  	block, _ := json.Marshal(map[string]any{
   167  		"type":    "fallback",
   168  		"from":    map[string]any{"model": s.from},
   169  		"to":      map[string]any{"model": s.to},
   170  		"trigger": map[string]any{"type": "refusal", "category": s.category},
   171  	})
   172  	return block
   173  }
   174  
   175  type heldRefusal struct {
   176  	startRaw      []byte // raw message_start data; nil if none arrived
   177  	deltaRaw      []byte // raw terminal message_delta data
   178  	stopRaw       []byte // raw message_stop data
   179  	transformsRaw []byte // input_transformations to forward; non-nil only when a suppressed message_start carried one
   180  }
   181  
   182  // streamSplicer is an io.ReadCloser forwarding the current hop's SSE stream,
   183  // retrying refusals down the chain, and splicing retries' events into one
   184  // app-visible stream.
   185  type streamSplicer struct {
   186  	out  bytes.Buffer
   187  	done bool
   188  	err  error
   189  
   190  	res *http.Response
   191  	dec ssestream.Decoder
   192  
   193  	body      map[string]json.RawMessage
   194  	fallbacks []anthropic.BetaFallbackParam
   195  	next      int // next chain index to try
   196  	state     *BetaFallbackState
   197  	send      func(body []byte) (*http.Response, error)
   198  
   199  	// Output framing.
   200  	wireOpen     bool
   201  	nextIndex    int // next free output block index
   202  	hopOffset    int // output index of the current hop's wire index 0
   203  	pendingSeams []seam
   204  	primaryID    string // first hop's message id; spliced starts adopt it
   205  
   206  	// Current hop.
   207  	hopModel    string // model string the hop was requested as
   208  	effModel    string // last seam's to.model within the hop, else hopModel
   209  	isRetryHop  bool
   210  	startRaw    []byte // held message_start data (emitted when the wire opens)
   211  	contentSeen bool
   212  	open        map[int]bool
   213  	blocks      map[int]*rawBlock
   214  	blockOrder  []int
   215  	sawSeam     bool
   216  	// startTransforms is message.input_transformations from a message_start
   217  	// the open wire suppressed; the hop's terminal message_delta carries it
   218  	// instead, as a server-side mid-stream fallback does.
   219  	startTransforms []byte
   220  
   221  	// Chain state.
   222  	token          string
   223  	category       any // policy category from the most recent refusal's stop_details; nil when not surfaced
   224  	continuation   []json.RawMessage
   225  	lastClaimCount int
   226  	ledger         []json.RawMessage
   227  	held           *heldRefusal
   228  	terminalSent   bool // a terminal message_delta reached the consumer
   229  	stopSent       bool // a message_stop reached the consumer
   230  
   231  	// lastHopStatus is the HTTP status of the most recent failed hop
   232  	// attempt, 0 for transport errors.
   233  	lastHopStatus int
   234  }
   235  
   236  func (s *streamSplicer) Read(p []byte) (int, error) {
   237  	for s.out.Len() == 0 && !s.done {
   238  		s.advance()
   239  	}
   240  	if s.out.Len() > 0 {
   241  		return s.out.Read(p)
   242  	}
   243  	if s.err != nil {
   244  		return 0, s.err
   245  	}
   246  	return 0, io.EOF
   247  }
   248  
   249  func (s *streamSplicer) Close() error {
   250  	s.done = true
   251  	if s.res != nil {
   252  		return s.res.Body.Close()
   253  	}
   254  	return nil
   255  }
   256  
   257  func (s *streamSplicer) finish(err error) {
   258  	s.done = true
   259  	s.err = err
   260  	if s.res != nil {
   261  		s.res.Body.Close()
   262  	}
   263  }
   264  
   265  func (s *streamSplicer) emit(eventType string, data []byte) {
   266  	fmt.Fprintf(&s.out, "event: %s\ndata: %s\n\n", eventType, bytes.TrimRight(data, "\n"))
   267  }
   268  
   269  // advance consumes one event from the current hop's stream.
   270  func (s *streamSplicer) advance() {
   271  	if !s.dec.Next() {
   272  		err := s.dec.Err()
   273  		if err == nil && s.terminalSent && !s.stopSent {
   274  			// The hop's wire ended without dispatching message_stop (an
   275  			// unterminated final frame); the spliced stream still completes.
   276  			s.emit("message_stop", []byte(`{"type": "message_stop"}`))
   277  			s.stopSent = true
   278  		}
   279  		s.finish(err)
   280  		return
   281  	}
   282  	evt := s.dec.Event()
   283  	if evt.Type == "message_stop" {
   284  		s.stopSent = true
   285  	}
   286  	switch evt.Type {
   287  	case "ping":
   288  		// Dropped: a held message_start must stay the first event out.
   289  	case "message_start":
   290  		s.startRaw = append([]byte(nil), evt.Data...)
   291  		if transforms := gjson.GetBytes(evt.Data, "message.input_transformations"); s.wireOpen && transforms.Exists() {
   292  			s.startTransforms = []byte(transforms.Raw)
   293  		}
   294  		if !s.isRetryHop {
   295  			s.primaryID = gjson.GetBytes(evt.Data, "message.id").String()
   296  		}
   297  	case "content_block_start":
   298  		s.ensureWireOpen()
   299  		index := int(gjson.GetBytes(evt.Data, "index").Int())
   300  		s.contentSeen = true
   301  		s.open[index] = true
   302  		block := gjson.GetBytes(evt.Data, "content_block")
   303  		b := &rawBlock{start: []byte(block.Raw), blockType: block.Get("type").String()}
   304  		if b.blockType == "fallback" {
   305  			s.sawSeam = true
   306  			s.effModel = block.Get("to.model").String()
   307  		}
   308  		s.blocks[index] = b
   309  		s.blockOrder = append(s.blockOrder, index)
   310  		s.emitReindexed(evt, index)
   311  	case "content_block_delta":
   312  		s.ensureWireOpen()
   313  		index := int(gjson.GetBytes(evt.Data, "index").Int())
   314  		s.contentSeen = true
   315  		s.foldDelta(index, evt.Data)
   316  		s.emitReindexed(evt, index)
   317  	case "content_block_stop":
   318  		s.ensureWireOpen()
   319  		index := int(gjson.GetBytes(evt.Data, "index").Int())
   320  		s.contentSeen = true
   321  		delete(s.open, index)
   322  		s.emitReindexed(evt, index)
   323  	case "message_delta":
   324  		s.handleTerminal(evt)
   325  	default:
   326  		// message_stop and anything unrecognised pass through.
   327  		s.emit(evt.Type, evt.Data)
   328  	}
   329  }
   330  
   331  // ensureWireOpen emits the held message_start and any queued seam blocks the
   332  // first time output framing is needed.
   333  func (s *streamSplicer) ensureWireOpen() {
   334  	if s.wireOpen {
   335  		return
   336  	}
   337  	s.wireOpen = true
   338  	start := s.startRaw
   339  	if start == nil {
   340  		return
   341  	}
   342  	if s.isRetryHop && s.primaryID != "" {
   343  		// The envelope keeps the primary's message id no matter which hop
   344  		// opens the wire.
   345  		if patched, err := sjson.SetBytes(start, "message.id", s.primaryID); err == nil {
   346  			start = patched
   347  		}
   348  	}
   349  	s.emit("message_start", start)
   350  	for _, sm := range s.pendingSeams {
   351  		s.emitSeam(sm)
   352  	}
   353  	s.pendingSeams = nil
   354  	s.hopOffset = s.nextIndex
   355  }
   356  
   357  func (s *streamSplicer) emitSeam(sm seam) {
   358  	index := s.nextIndex
   359  	s.nextIndex++
   360  	start, _ := json.Marshal(map[string]any{
   361  		"type":          "content_block_start",
   362  		"index":         index,
   363  		"content_block": sm.block(),
   364  	})
   365  	s.emit("content_block_start", start)
   366  	stop, _ := json.Marshal(map[string]any{"type": "content_block_stop", "index": index})
   367  	s.emit("content_block_stop", stop)
   368  }
   369  
   370  // emitReindexed forwards a block event, remapping its index into output
   371  // space. Other fields keep their wire bytes.
   372  func (s *streamSplicer) emitReindexed(evt ssestream.Event, wireIndex int) {
   373  	out := s.hopOffset + wireIndex
   374  	if out+1 > s.nextIndex {
   375  		s.nextIndex = out + 1
   376  	}
   377  	data := evt.Data
   378  	if out != wireIndex {
   379  		if patched, err := sjson.SetBytes(data, "index", out); err == nil {
   380  			data = patched
   381  		}
   382  	}
   383  	s.emit(evt.Type, data)
   384  }
   385  
   386  func (s *streamSplicer) foldDelta(index int, data []byte) {
   387  	b := s.blocks[index]
   388  	if b == nil {
   389  		return
   390  	}
   391  	delta := gjson.GetBytes(data, "delta")
   392  	switch delta.Get("type").String() {
   393  	case "text_delta":
   394  		b.text.WriteString(delta.Get("text").String())
   395  	case "thinking_delta":
   396  		b.thinking.WriteString(delta.Get("thinking").String())
   397  	case "signature_delta":
   398  		b.signature.WriteString(delta.Get("signature").String())
   399  	case "input_json_delta":
   400  		b.partial.WriteString(delta.Get("partial_json").String())
   401  	case "citations_delta":
   402  		if c := delta.Get("citation"); c.Exists() {
   403  			b.citations = append(b.citations, json.RawMessage(c.Raw))
   404  		}
   405  	}
   406  }
   407  
   408  // closeOpenBlocks synthesizes content_block_stop for blocks a refusal cut
   409  // mid-stream, in ascending index order, so the boundary lands on complete
   410  // framing.
   411  func (s *streamSplicer) closeOpenBlocks() {
   412  	if len(s.open) == 0 {
   413  		return
   414  	}
   415  	indices := make([]int, 0, len(s.open))
   416  	for index := range s.open {
   417  		indices = append(indices, index)
   418  	}
   419  	sort.Ints(indices)
   420  	for _, index := range indices {
   421  		data, _ := json.Marshal(map[string]any{"type": "content_block_stop", "index": s.hopOffset + index})
   422  		s.emit("content_block_stop", data)
   423  	}
   424  	s.open = map[int]bool{}
   425  }
   426  
   427  // handleTerminal decides what a hop's message_delta means: serve, retry, or
   428  // surface.
   429  func (s *streamSplicer) handleTerminal(evt ssestream.Event) {
   430  	parsed := gjson.ParseBytes(evt.Data)
   431  	refused := parsed.Get("delta.stop_reason").String() == string(anthropic.BetaStopReasonRefusal)
   432  
   433  	if !refused {
   434  		s.ensureWireOpen()
   435  		s.terminalSent = true
   436  		if !s.isRetryHop {
   437  			// The original attempt served: the middleware is the identity.
   438  			s.emit(evt.Type, evt.Data)
   439  			return
   440  		}
   441  		s.emit(evt.Type, s.rewriteTerminal(evt.Data, true, false))
   442  		return
   443  	}
   444  
   445  	token := parsed.Get("delta.stop_details.fallback_credit_token").String()
   446  	// Token required once content has streamed; a pre-stream refusal retries
   447  	// free — nothing reached the consumer.
   448  	canRetry := token != "" || !s.contentSeen
   449  
   450  	if s.next < len(s.fallbacks) && canRetry {
   451  		s.ledger = append(s.ledger, s.hopIterations(evt.Data, false)...)
   452  		s.lastClaimCount = 0
   453  		if parsed.Get("delta.stop_details.fallback_has_prefill_claim").Type == gjson.True {
   454  			claim := s.claimBlocks()
   455  			s.continuation = append(s.continuation, claim...)
   456  			s.lastClaimCount = len(claim)
   457  		}
   458  		s.token = token
   459  		s.category = nil
   460  		if cat := parsed.Get("delta.stop_details.category"); cat.Type == gjson.String {
   461  			s.category = cat.String()
   462  		}
   463  		s.held = &heldRefusal{startRaw: s.startRaw, deltaRaw: append([]byte(nil), evt.Data...), stopRaw: s.readStop(), transformsRaw: s.startTransforms}
   464  		s.res.Body.Close()
   465  		s.openNextHop()
   466  		return
   467  	}
   468  
   469  	if !s.isRetryHop {
   470  		// Untouched surface: no retry ever engaged, so the original wire
   471  		// bytes pass through, terminal included.
   472  		s.ensureWireOpen()
   473  		s.terminalSent = true
   474  		s.emit(evt.Type, evt.Data)
   475  		return
   476  	}
   477  
   478  	// A retry hop's refusal surfaces as the chain's terminal: merged
   479  	// iterations with this hop as the fallback_message completer, and
   480  	// recommended_model stamped null when the wire left it out.
   481  	s.ensureWireOpen()
   482  	s.terminalSent = true
   483  	s.emit(evt.Type, s.rewriteTerminal(evt.Data, true, true))
   484  }
   485  
   486  // readStop reads the message_stop that follows a held refusal delta.
   487  func (s *streamSplicer) readStop() []byte {
   488  	for s.dec.Next() {
   489  		evt := s.dec.Event()
   490  		if evt.Type == "ping" {
   491  			continue
   492  		}
   493  		if evt.Type == "message_stop" {
   494  			return append([]byte(nil), evt.Data...)
   495  		}
   496  		break
   497  	}
   498  	data, _ := json.Marshal(map[string]any{"type": "message_stop"})
   499  	return data
   500  }
   501  
   502  // rewriteTerminal rebuilds a terminal message_delta: usage.iterations becomes
   503  // the whole-chain ledger (plus this hop, last entry typed fallback_message),
   504  // optionally recommended_model is stamped null when absent, and a suppressed
   505  // message_start's input_transformations is forwarded when the delta has none.
   506  func (s *streamSplicer) rewriteTerminal(data []byte, includeSelf bool, stampRecommended bool) []byte {
   507  	iterations := append([]json.RawMessage{}, s.ledger...)
   508  	if includeSelf {
   509  		iterations = append(iterations, s.hopIterations(data, true)...)
   510  	}
   511  	out := data
   512  	if patched, err := sjson.SetRawBytes(out, "usage.iterations", mustMarshal(iterations)); err == nil {
   513  		out = patched
   514  	}
   515  	if stampRecommended && !gjson.GetBytes(out, "delta.stop_details.recommended_model").Exists() {
   516  		if patched, err := sjson.SetRawBytes(out, "delta.stop_details.recommended_model", []byte("null")); err == nil {
   517  			out = patched
   518  		}
   519  	}
   520  	if s.startTransforms != nil && !gjson.GetBytes(out, "input_transformations").Exists() {
   521  		if patched, err := sjson.SetRawBytes(out, "input_transformations", s.startTransforms); err == nil {
   522  			out = patched
   523  		}
   524  	}
   525  	return out
   526  }
   527  
   528  // hopIterations builds the current hop's usage.iterations contribution from
   529  // its terminal delta. A hop reporting its own iterations (a server-stitched
   530  // envelope, or a server-tool loop) contributes them verbatim; otherwise one
   531  // entry is synthesized from its delta usage. Every non-final entry is
   532  // (re)typed message — those attempts did not serve; the final hop's last
   533  // entry is the fallback_message completer. The hop's model is stamped only
   534  // when the contribution is unambiguously the hop itself: a single entry. A
   535  // multi-entry array attributes per-iteration usage the wire did not break
   536  // down by model.
   537  func (s *streamSplicer) hopIterations(deltaData []byte, final bool) []json.RawMessage {
   538  	var entries []json.RawMessage
   539  	if reported := gjson.GetBytes(deltaData, "usage.iterations"); reported.IsArray() && len(reported.Array()) > 0 {
   540  		for _, item := range reported.Array() {
   541  			entries = append(entries, json.RawMessage(item.Raw))
   542  		}
   543  	} else {
   544  		entries = []json.RawMessage{synthesizedEntry(deltaData)}
   545  	}
   546  	for i, entry := range entries {
   547  		last := i == len(entries)-1
   548  		typ := gjson.GetBytes(entry, "type").String()
   549  		switch {
   550  		case final && last:
   551  			typ = "fallback_message"
   552  		case typ == "fallback_message":
   553  			// The envelope's serving hop declined the overall request; it is
   554  			// an ordinary attempt in the merged ledger.
   555  			typ = "message"
   556  		case typ == "":
   557  			typ = "message"
   558  		}
   559  		patched, err := sjson.SetBytes(entry, "type", typ)
   560  		if err != nil {
   561  			continue
   562  		}
   563  		// Entries the hop supplied unlabeled can only be the hop's own
   564  		// attempts; a server-stitched envelope labels its merged ledger.
   565  		if !gjson.GetBytes(patched, "model").Exists() {
   566  			if p, err := sjson.SetBytes(patched, "model", s.hopModel); err == nil {
   567  				patched = p
   568  			}
   569  		}
   570  		entries[i] = patched
   571  	}
   572  	return entries
   573  }
   574  
   575  // synthesizedEntry derives one ledger entry from a hop that reported no
   576  // iterations: the base usage fields from its terminal delta, zero where the
   577  // delta left them out and null cache_creation — message_start's snapshot is
   578  // a running total, not this attempt's bill.
   579  func synthesizedEntry(deltaData []byte) json.RawMessage {
   580  	delta := gjson.GetBytes(deltaData, "usage")
   581  	entry := []byte(`{}`)
   582  	for _, key := range []string{"input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"} {
   583  		raw := "0"
   584  		if value := delta.Get(key); value.Exists() {
   585  			raw = value.Raw
   586  		}
   587  		if patched, err := sjson.SetRawBytes(entry, key, []byte(raw)); err == nil {
   588  			entry = patched
   589  		}
   590  	}
   591  	raw := "null"
   592  	if value := delta.Get("cache_creation"); value.Exists() {
   593  		raw = value.Raw
   594  	}
   595  	if patched, err := sjson.SetRawBytes(entry, "cache_creation", []byte(raw)); err == nil {
   596  		entry = patched
   597  	}
   598  	entry, _ = sjson.SetBytes(entry, "type", "message")
   599  	return entry
   600  }
   601  
   602  // claimBlocks reconstructs the refused hop's streamed content for a prefill
   603  // claim. Fallback seam blocks never ride a claim; when the hop was itself a
   604  // stitched envelope only the content after its last seam is the partial, and
   605  // its trailing whitespace is the server's, not the model's.
   606  func (s *streamSplicer) claimBlocks() []json.RawMessage {
   607  	var finalized []json.RawMessage
   608  	for _, index := range s.blockOrder {
   609  		b := s.blocks[index]
   610  		if b.blockType == "fallback" {
   611  			finalized = nil // restart after the seam
   612  			continue
   613  		}
   614  		finalized = append(finalized, b.finalize())
   615  	}
   616  	if s.sawSeam && len(finalized) > 0 {
   617  		last := finalized[len(finalized)-1]
   618  		if gjson.GetBytes(last, "type").String() == "text" {
   619  			text := gjson.GetBytes(last, "text").String()
   620  			if trimmed := strings.TrimRight(text, " \t\r\n"); trimmed != text {
   621  				if patched, err := sjson.SetBytes(last, "text", trimmed); err == nil {
   622  					finalized[len(finalized)-1] = patched
   623  				}
   624  			}
   625  		}
   626  	}
   627  	return finalized
   628  }
   629  
   630  // hopBody builds one retry attempt: the original body with the entry merged
   631  // over it, the credit token attached, and the continuation appended as one
   632  // trailing assistant turn.
   633  func (s *streamSplicer) hopBody(entry anthropic.BetaFallbackParam, token string, continuation []json.RawMessage) ([]byte, error) {
   634  	merged, _, err := mergeFallback(s.body, entry, token)
   635  	if err != nil {
   636  		return nil, err
   637  	}
   638  	if len(continuation) == 0 {
   639  		return merged, nil
   640  	}
   641  	turn, err := json.Marshal(map[string]any{"role": "assistant", "content": continuation})
   642  	if err != nil {
   643  		return nil, err
   644  	}
   645  	var messages []json.RawMessage
   646  	gjson.GetBytes(merged, "messages").ForEach(func(_, msg gjson.Result) bool {
   647  		messages = append(messages, json.RawMessage(msg.Raw))
   648  		return true
   649  	})
   650  	messages = append(messages, turn)
   651  	return sjson.SetRawBytes(merged, "messages", mustMarshal(messages))
   652  }
   653  
   654  // openNextHop walks the remaining chain after a held refusal. Failed hops
   655  // are skipped with the token and continuation intact. A 400 must not
   656  // surface a refusal the app never saw, so the last entry descends a ladder,
   657  // shedding one ingredient per rung: first the echoed partial goes (the
   658  // appended assistant turn may be all the server rejected), then the token
   659  // (it is not consumed by a rejected redemption; only the reprice is lost).
   660  // A chain that never engages degrades to the held refusal.
   661  func (s *streamSplicer) openNextHop() {
   662  	lastStatus := 0
   663  	var lastEntry anthropic.BetaFallbackParam
   664  	for s.next < len(s.fallbacks) {
   665  		entry := s.fallbacks[s.next]
   666  		index := s.next
   667  		s.next++
   668  		if s.state != nil {
   669  			s.state.SetIndex(index)
   670  		}
   671  		res, err := s.tryHop(entry, s.token, s.continuation)
   672  		if err != nil {
   673  			s.finish(err)
   674  			return
   675  		}
   676  		if res != nil {
   677  			s.engage(res, entry)
   678  			return
   679  		}
   680  		lastStatus = s.lastHopStatus
   681  		lastEntry = entry
   682  	}
   683  
   684  	// The server adjudicates a claim; its 400 may reject only the appended
   685  	// turn, so the last entry gets one attempt without this hop's partial.
   686  	if lastStatus == http.StatusBadRequest && s.lastClaimCount > 0 {
   687  		trimmed := s.continuation[:len(s.continuation)-s.lastClaimCount]
   688  		res, err := s.tryHop(lastEntry, s.token, trimmed)
   689  		if err != nil {
   690  			s.finish(err)
   691  			return
   692  		}
   693  		if res != nil {
   694  			s.continuation = trimmed
   695  			s.lastClaimCount = 0
   696  			s.engage(res, lastEntry)
   697  			return
   698  		}
   699  		lastStatus = s.lastHopStatus
   700  	}
   701  
   702  	// Final rung: same entry, no echoed partial, no token. A rejected
   703  	// redemption does not consume the token, so dropping it costs only the
   704  	// reprice — never the conversation.
   705  	if lastStatus == http.StatusBadRequest && s.token != "" {
   706  		trimmed := s.continuation[:len(s.continuation)-s.lastClaimCount]
   707  		res, err := s.tryHop(lastEntry, "", trimmed)
   708  		if err != nil {
   709  			s.finish(err)
   710  			return
   711  		}
   712  		if res != nil {
   713  			s.token = ""
   714  			s.continuation = trimmed
   715  			s.lastClaimCount = 0
   716  			s.engage(res, lastEntry)
   717  			return
   718  		}
   719  		lastStatus = s.lastHopStatus
   720  	}
   721  
   722  	s.degrade(lastStatus, lastEntry)
   723  }
   724  
   725  // tryHop issues one hop attempt. It returns the response when the hop
   726  // engaged (a readable SSE stream), nil when the hop failed and the chain
   727  // should move on, or an error for unrecoverable problems building the
   728  // attempt.
   729  func (s *streamSplicer) tryHop(entry anthropic.BetaFallbackParam, token string, continuation []json.RawMessage) (*http.Response, error) {
   730  	payload, err := s.hopBody(entry, token, continuation)
   731  	if err != nil {
   732  		return nil, err
   733  	}
   734  	res, err := s.send(payload)
   735  	if err != nil {
   736  		s.lastHopStatus = 0
   737  		return nil, nil
   738  	}
   739  	if !isEventStream(res) {
   740  		s.lastHopStatus = res.StatusCode
   741  		res.Body.Close()
   742  		return nil, nil
   743  	}
   744  	return res, nil
   745  }
   746  
   747  // engage switches the splicer onto a hop that returned a live stream,
   748  // emitting (or queueing) the fallback boundary between the previous
   749  // content-bearing model and this hop.
   750  func (s *streamSplicer) engage(res *http.Response, entry anthropic.BetaFallbackParam) {
   751  	sm := seam{from: s.effModel, to: string(entry.Model), category: s.category}
   752  	if s.wireOpen {
   753  		s.closeOpenBlocks()
   754  		s.emitSeam(sm)
   755  		s.hopOffset = s.nextIndex
   756  	} else {
   757  		s.pendingSeams = append(s.pendingSeams, sm)
   758  	}
   759  
   760  	s.hopModel = string(entry.Model)
   761  	s.effModel = s.hopModel
   762  	s.isRetryHop = true
   763  	s.contentSeen = false
   764  	s.sawSeam = false
   765  	s.open = map[int]bool{}
   766  	s.blocks = map[int]*rawBlock{}
   767  	s.blockOrder = nil
   768  	s.startRaw = nil
   769  	s.startTransforms = nil
   770  
   771  	s.res = res
   772  	s.dec = ssestream.NewDecoder(res)
   773  }
   774  
   775  // degrade surfaces the held refusal after every remaining hop failed: its
   776  // terminal events replay with the chain's ledger, a recommended_model
   777  // verdict — the unreachable entry on a rate limit, null otherwise — and a
   778  // suppressed message_start's input_transformations when the delta has none.
   779  func (s *streamSplicer) degrade(lastStatus int, lastEntry anthropic.BetaFallbackParam) {
   780  	held := s.held
   781  	if held == nil {
   782  		s.finish(nil)
   783  		return
   784  	}
   785  	if !s.wireOpen {
   786  		s.wireOpen = true
   787  		start := held.startRaw
   788  		if start != nil {
   789  			if s.primaryID != "" {
   790  				if patched, err := sjson.SetBytes(start, "message.id", s.primaryID); err == nil {
   791  					start = patched
   792  				}
   793  			}
   794  			s.emit("message_start", start)
   795  		}
   796  		for _, sm := range s.pendingSeams {
   797  			s.emitSeam(sm)
   798  		}
   799  		s.pendingSeams = nil
   800  	}
   801  
   802  	recommended := []byte("null")
   803  	if lastStatus == http.StatusTooManyRequests {
   804  		recommended, _ = json.Marshal(string(lastEntry.Model))
   805  	}
   806  	data := held.deltaRaw
   807  	existing := gjson.GetBytes(data, "delta.stop_details.recommended_model")
   808  	if !existing.Exists() || existing.Type == gjson.Null {
   809  		if patched, err := sjson.SetRawBytes(data, "delta.stop_details.recommended_model", recommended); err == nil {
   810  			data = patched
   811  		}
   812  	}
   813  	if len(s.ledger) > 0 {
   814  		if patched, err := sjson.SetRawBytes(data, "usage.iterations", mustMarshal(s.ledger)); err == nil {
   815  			data = patched
   816  		}
   817  	}
   818  	if held.transformsRaw != nil && !gjson.GetBytes(data, "input_transformations").Exists() {
   819  		if patched, err := sjson.SetRawBytes(data, "input_transformations", held.transformsRaw); err == nil {
   820  			data = patched
   821  		}
   822  	}
   823  	s.emit("message_delta", data)
   824  	s.emit("message_stop", held.stopRaw)
   825  	s.finish(nil)
   826  }
   827  
   828  func mustMarshal(v any) []byte {
   829  	data, err := json.Marshal(v)
   830  	if err != nil {
   831  		return []byte("null")
   832  	}
   833  	return data
   834  }