ituoke/internal/agent/stream.go

199 lines
5.8 KiB
Go

package agent
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
func completeStream(ctx context.Context, c ModelConfig, key string, messages []modelMessage, onDelta func(string), onReasoning ...func(string)) (string, string, error) {
res, err := openModelRequest(ctx, c, key, http.MethodPost, "/chat/completions", completionBody(c, messages, true))
if err != nil {
return "", "", err
}
defer res.Body.Close()
if !strings.HasPrefix(strings.ToLower(res.Header.Get("Content-Type")), "text/event-stream") {
return "", "", fmt.Errorf("模型服务没有返回流式响应,请确认所选接口支持 stream=true")
}
step, err := readToolStream(res.Body, key, onDelta, onReasoning...)
text, reason := step.Text, step.Reason
if ctx.Err() != nil {
return text, reason, ctx.Err()
}
return text, reason, err
}
// SSE data events may span TCP reads or multiple data: lines. Completion markers
// distinguish a finished answer from a connection that silently ended mid-answer.
func readCompletionStream(reader io.Reader, key string, onDelta func(string)) (string, string, error) {
result, err := readToolStream(reader, key, onDelta)
return result.Text, result.Reason, err
}
type completionStep struct {
Text string
Reason string
Reasoning string
Calls []modelToolCall
}
func readToolStream(reader io.Reader, key string, onDelta func(string), onReasoning ...func(string)) (completionStep, error) {
scanner := bufio.NewScanner(io.LimitReader(reader, 8*1024*1024+1))
scanner.Buffer(make([]byte, 4096), 512*1024)
var text strings.Builder
var reasoning strings.Builder
var data []string
finishReason := ""
done := false
calls := map[int]*modelToolCall{}
result := func() completionStep {
step := completionStep{Text: text.String(), Reason: finishReason, Reasoning: reasoning.String()}
for i := 0; i < len(calls); i++ {
if calls[i] != nil {
step.Calls = append(step.Calls, *calls[i])
}
}
return step
}
dispatch := func() error {
payload := strings.Join(data, "\n")
data = nil
if payload == "" {
return nil
}
if payload == "[DONE]" {
done = true
return nil
}
var chunk struct {
Error json.RawMessage `json:"error"`
Choices []struct {
Index int `json:"index"`
Delta struct {
ReasoningContent string `json:"reasoning_content"`
Reasoning string `json:"reasoning"`
Content string `json:"content"`
Refusal string `json:"refusal"`
ToolCalls []struct {
Index int `json:"index"`
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(payload), &chunk); err != nil {
return fmt.Errorf("模型流式响应格式无效,已保留收到的内容")
}
if len(chunk.Error) > 0 && string(chunk.Error) != "null" {
return modelHTTPError(200, []byte(payload), key)
}
for _, choice := range chunk.Choices {
if choice.Index != 0 {
continue
}
for _, delta := range choice.Delta.ToolCalls {
if delta.Index < 0 {
return fmt.Errorf("模型工具调用索引无效")
}
call := calls[delta.Index]
if call == nil {
call = &modelToolCall{Type: "function"}
calls[delta.Index] = call
}
if delta.ID != "" {
call.ID = delta.ID
}
if delta.Type != "" {
call.Type = delta.Type
}
call.Function.Name += delta.Function.Name
call.Function.Arguments += delta.Function.Arguments
if len(call.Function.Name) > 128 || len(call.Function.Arguments) > 64*1024 {
return fmt.Errorf("工具调用参数过大")
}
}
delta := choice.Delta.Content
if delta == "" {
delta = choice.Delta.Refusal
}
thought := choice.Delta.ReasoningContent
if thought == "" {
thought = choice.Delta.Reasoning
}
if text.Len()+reasoning.Len()+len(delta)+len(thought) > 4*1024*1024 {
return fmt.Errorf("模型输出过大,已停止接收")
}
if thought != "" {
reasoning.WriteString(thought)
for _, notify := range onReasoning {
if notify != nil {
notify(thought)
}
}
}
if delta != "" {
text.WriteString(delta)
if onDelta != nil {
onDelta(delta)
}
}
if choice.FinishReason != "" {
finishReason = choice.FinishReason
}
}
return nil
}
for scanner.Scan() {
line := strings.TrimPrefix(scanner.Text(), "\ufeff")
if line == "" {
if err := dispatch(); err != nil {
return result(), err
}
if done {
break
}
} else if value, ok := strings.CutPrefix(line, "data:"); ok {
data = append(data, strings.TrimPrefix(value, " "))
}
}
if err := scanner.Err(); err != nil {
return result(), fmt.Errorf("模型连接中断或超时,已保留收到的内容")
}
if len(data) > 0 {
if err := dispatch(); err != nil {
return result(), err
}
}
if !done && finishReason == "" {
return result(), fmt.Errorf("模型连接提前结束,回答可能不完整")
}
if finishReason == "content_filter" {
return result(), fmt.Errorf("模型输出被服务商过滤,已保留收到的内容")
}
if len(calls) > 0 {
for i := 0; i < len(calls); i++ {
call := calls[i]
if call == nil || call.ID == "" || call.Type != "function" || call.Function.Name == "" || !json.Valid([]byte(call.Function.Arguments)) {
return result(), fmt.Errorf("模型工具调用不完整,未执行")
}
}
if finishReason != "tool_calls" && finishReason != "stop" {
return result(), fmt.Errorf("工具调用未正常结束,未执行")
}
}
if text.Len() == 0 && len(calls) == 0 {
return result(), fmt.Errorf("模型没有返回文字,请检查模型能力或输出上限")
}
return result(), nil
}