mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
feat: OpenAI 兼容端点完整实现 + reasoning_content 输出通道
- 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 回报而非估算
This commit is contained in:
@ -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
|
||||
|
||||
@ -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"})
|
||||
|
||||
@ -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() }
|
||||
|
||||
Reference in New Issue
Block a user