dispatchDelta forwards reasoning and content as events, then accumulates streamed tool-call fragments into index-keyed slots. Reasoning stays out of fullContent (must not round-trip into the assistant message) but is forwarded so the UI reflects thinking. Fragments key on the provider's `index`, not
(parent context.Context, d streamDelta, budget cloud.BudgetStatus, fullContent *strings.Builder, slots map[int]*toolSlot, order *[]int, out chan<- Event)
| 542 | var ( |
| 543 | fullContent strings.Builder |
| 544 | slots = map[int]*toolSlot{} |
| 545 | order []int |
| 546 | tokens int |
| 547 | promptTokens int |
| 548 | ) |
| 549 | |
| 550 | for scanner.Scan() { |
| 551 | // Any line (data, blank separator, or ": keepalive" comment) is |
| 552 | // liveness; reset the idle watchdog before inspecting it. |
| 553 | onFrame() |
| 554 | line := bytes.TrimSpace(scanner.Bytes()) |
| 555 | if len(line) == 0 || !bytes.HasPrefix(line, []byte("data:")) { |
| 556 | continue |
| 557 | } |
| 558 | payload := bytes.TrimSpace(line[len("data:"):]) |
| 559 | if bytes.Equal(payload, []byte("[DONE]")) { |
| 560 | break |
| 561 | } |
| 562 | var sc streamChunk |
| 563 | if err := json.Unmarshal(payload, &sc); err != nil { |
| 564 | continue |
| 565 | } |
| 566 | if sc.Error != nil { |
| 567 | msg := sc.Error.Message |
| 568 | if msg == "" { |
| 569 | msg = string(payload) |
| 570 | } |
| 571 | return nil, 0, 0, fmt.Errorf("the server reported a stream error: %s", msg) |
| 572 | } |
| 573 | for _, choice := range sc.Choices { |
| 574 | if !dispatchDelta(parent, choice.Delta, budget, &fullContent, slots, &order, out) { |
| 575 | return nil, 0, 0, parent.Err() |
| 576 | } |
| 577 | } |
| 578 | if sc.Usage != nil { |
| 579 | tokens = sc.Usage.CompletionTokens |
| 580 | promptTokens = sc.Usage.PromptTokens |
| 581 | } |
| 582 | } |
| 583 | if err := scanner.Err(); err != nil { |
| 584 | return nil, 0, 0, err |
| 585 | } |
| 586 | |
| 587 | // Emit accumulated tool calls once at stream end, independent of |