package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net/http" "strings" ) // deltaSink receives streamed text fragments for a given model panel. type deltaSink func(idx int, text string) type streamDelta struct { Content string `json:"content"` ReasoningContent string `json:"reasoning_content"` } type streamChoice struct { Delta streamDelta `json:"delta"` } type streamChunk struct { Choices []streamChoice `json:"choices"` } // sendRequest streams a chat completion and forwards each content/reasoning // delta to sink. Reasoning and assistant content are emitted into the same // stream so they render together. func sendRequest(baseURL, model, prompt string, maxTokens, idx int, sink deltaSink) error { payload := map[string]any{ "model": model, "messages": []map[string]string{ {"role": "user", "content": prompt}, }, "max_tokens": maxTokens, "stream": true, } body, err := json.Marshal(payload) if err != nil { return err } resp, err := http.Post(baseURL+"/v1/chat/completions", "application/json", bytes.NewReader(body)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(b))) } scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data:") { continue } data := strings.TrimSpace(strings.TrimPrefix(line, "data:")) if data == "" || data == "[DONE]" { if data == "[DONE]" { break } continue } var chunk streamChunk if err := json.Unmarshal([]byte(data), &chunk); err != nil { continue } for _, c := range chunk.Choices { if c.Delta.ReasoningContent != "" { sink(idx, c.Delta.ReasoningContent) } if c.Delta.Content != "" { sink(idx, c.Delta.Content) } } } return scanner.Err() }