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 }