199 lines
5.8 KiB
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
|
|
}
|