From d4956c23f0a2dee2bdbc4fe7b7e75fc960356c6e Mon Sep 17 00:00:00 2001 From: root Date: Fri, 3 Jul 2026 20:49:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OpenAI=20=E5=85=BC=E5=AE=B9=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0=20+=20reasoning?= =?UTF-8?q?=5Fcontent=20=E8=BE=93=E5=87=BA=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StageContext 新增 ReasoningContent + TokenUsage 字段 - emitResponse 将 reasoning_content / usage 传入 Payload - /v1/chat/completions: - 非流式返回 reasoning_content + token_usage - 流式 (stream=true) SSE 分块返回 reasoning/content/finish chunk - token_usage 来自精确 LLM 回报而非估算 --- internal/agent/core/agent.go | 20 +++++- internal/plugins/webui/handler.go | 103 ++++++++++++++++++++++++++++-- internal/sdk/plugin.go | 28 ++++---- 3 files changed, 129 insertions(+), 22 deletions(-) diff --git a/internal/agent/core/agent.go b/internal/agent/core/agent.go index cf1f32b..9e2bd98 100644 --- a/internal/agent/core/agent.go +++ b/internal/agent/core/agent.go @@ -358,17 +358,25 @@ func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string) { ch = evt.Source } - a.io.EmitOutputTo(evt.Source, ch, "text", map[string]interface{}{ + payload := map[string]interface{}{ "content": response, "request_id": evt.RequestID, - }) + } + if stageCtx.ReasoningContent != "" { + payload["reasoning_content"] = stageCtx.ReasoningContent + } + if stageCtx.TokenUsage != nil { + payload["usage"] = stageCtx.TokenUsage + } + + a.io.EmitOutputTo(evt.Source, ch, "text", payload) if evt.ResponseCh != nil { evt.ResponseCh <- &agentIO.OutputEvent{ RequestID: evt.RequestID, Target: evt.Source, Type: "text", - Payload: map[string]interface{}{"content": response}, + Payload: payload, Done: true, OutputChannel: ch, } @@ -459,6 +467,12 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri // === Stage: post_action — LLM 返回,插件可审查/修改 === stageCtx.LLMText = resp.Content + stageCtx.ReasoningContent = resp.ReasoningContent + stageCtx.TokenUsage = map[string]int{ + "prompt_tokens": resp.TokenUsage.Prompt, + "completion_tokens": resp.TokenUsage.Completion, + "total_tokens": resp.TokenUsage.Total, + } stageCtx.ToolCalls = convertToolCalls(resp.ToolCalls) if a.runStage(sdk.StagePostAction, stageCtx) { return *stageCtx.Response, toolsUsed, nil diff --git a/internal/plugins/webui/handler.go b/internal/plugins/webui/handler.go index 0d8da77..8d7d626 100644 --- a/internal/plugins/webui/handler.go +++ b/internal/plugins/webui/handler.go @@ -690,6 +690,15 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request return } + content, _ := response.Payload["content"].(string) + reasoningContent, _ := response.Payload["reasoning_content"].(string) + usage, _ := response.Payload["usage"].(map[string]interface{}) + + if req.Stream { + h.writeOpenAIStream(w, req.Model, content, reasoningContent, usage) + return + } + resp := map[string]interface{}{ "id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()), "object": "chat.completion", @@ -700,22 +709,104 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request "index": 0, "message": map[string]interface{}{ "role": "assistant", - "content": response.Payload["content"], + "content": content, }, "finish_reason": "stop", }, }, - "usage": map[string]interface{}{ - "prompt_tokens": len(lastMsg.Content) / 2, - "completion_tokens": len(fmt.Sprint(response.Payload["content"])) / 2, - "total_tokens": (len(lastMsg.Content) + len(fmt.Sprint(response.Payload["content"]))) / 2, - }, + } + if reasoningContent != "" { + resp["choices"].([]map[string]interface{})[0]["message"].(map[string]interface{})["reasoning_content"] = reasoningContent + } + if usage != nil { + resp["usage"] = usage } w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(resp) } +func (h *Handler) writeOpenAIStream(w http.ResponseWriter, model, content, reasoningContent string, usage map[string]interface{}) { + flusher, ok := w.(http.Flusher) + if !ok { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "streaming not supported"}) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.WriteHeader(http.StatusOK) + flusher.Flush() + + // 如果有 reasoning_content,先发送一个 reasoning chunk + if reasoningContent != "" { + reasoningChunk := map[string]interface{}{ + "id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()), + "object": "chat.completion.chunk", + "created": time.Now().Unix(), + "model": model, + "choices": []map[string]interface{}{ + { + "index": 0, + "delta": map[string]interface{}{ + "content": "", + "reasoning_content": reasoningContent, + }, + "finish_reason": nil, + }, + }, + } + data, _ := json.Marshal(reasoningChunk) + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + } + + // content chunk + contentChunk := map[string]interface{}{ + "id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()), + "object": "chat.completion.chunk", + "created": time.Now().Unix(), + "model": model, + "choices": []map[string]interface{}{ + { + "index": 0, + "delta": map[string]interface{}{ + "content": content, + }, + "finish_reason": nil, + }, + }, + } + data, _ := json.Marshal(contentChunk) + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + + // finish chunk + finishChunk := map[string]interface{}{ + "id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()), + "object": "chat.completion.chunk", + "created": time.Now().Unix(), + "model": model, + "choices": []map[string]interface{}{ + { + "index": 0, + "delta": map[string]interface{}{}, + "finish_reason": "stop", + }, + }, + } + if usage != nil { + finishChunk["usage"] = usage + } + data, _ = json.Marshal(finishChunk) + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + + fmt.Fprintf(w, "data: [DONE]\n\n") + flusher.Flush() +} + func (h *Handler) handleTracker(w http.ResponseWriter, r *http.Request) { if h.tracker == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "tracker not available"}) diff --git a/internal/sdk/plugin.go b/internal/sdk/plugin.go index 20dccbb..fc92999 100644 --- a/internal/sdk/plugin.go +++ b/internal/sdk/plugin.go @@ -30,19 +30,21 @@ const ( ) type StageContext struct { - mu sync.RWMutex - RawMessage string - UserID string - GroupID string - ContextMsgs []map[string]interface{} - LLMText string - ToolCalls []ToolCall - ToolResults []ToolResult - FinalText string - Response *string - Phase Stage - Memory []MemItem - Extra map[string]interface{} + mu sync.RWMutex + RawMessage string + UserID string + GroupID string + ContextMsgs []map[string]interface{} + LLMText string + ReasoningContent string + TokenUsage map[string]int + ToolCalls []ToolCall + ToolResults []ToolResult + FinalText string + Response *string + Phase Stage + Memory []MemItem + Extra map[string]interface{} } func (c *StageContext) RLock() { c.mu.RLock() }