package webui import ( "context" "fmt" "time" "encoding/json" agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" "net/http" ) // OpenAI 兼容面:/v1/chat/completions(含流式)。 func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if h.sdk == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "IO manager not available"}) return } var req struct { Model string `json:"model"` Messages []openAIMessage `json:"messages"` Stream bool `json:"stream"` Temperature float64 `json:"temperature"` MaxTokens int `json:"max_tokens"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()}) return } if len(req.Messages) == 0 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "messages is required"}) return } lastMsg := req.Messages[len(req.Messages)-1] if lastMsg.Role != "user" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "last message must be from user"}) return } // 带超时的上下文(同 handleChat:客户端断开立即取消;300s 约束长生成) ctx, cancel := context.WithTimeout(r.Context(), 300*time.Second) defer cancel() respCh := make(chan *agentIO.OutputEvent, 1) go func() { // NoMemory:本端点(OpenAI 兼容 /v1/chat/completions)的调用方是 // IDE、工具与脚本,送来的是**固定的提示词模板**("请分析这段代码"之类), // 不是人类在对话。记进记忆会把真实对话挤掉,而且同一模板会反复刷屏。 // 原文仍进上下文,模型照旧看得到;只是不参与向量化/关键词提取/蒸馏。 respCh <- h.sdk.InjectTextSyncNoMemory("http", "http", lastMsg.Content) }() var response *agentIO.OutputEvent select { case response = <-respCh: case <-ctx.Done(): writeJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "agent timeout (300s)"}) return } if response == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"}) 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", "created": time.Now().Unix(), "model": req.Model, "choices": []map[string]interface{}{ { "index": 0, "message": map[string]interface{}{ "role": "assistant", "content": content, }, "finish_reason": "stop", }, }, } 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() } type openAIMessage struct { Role string `json:"role"` Content string `json:"content"` }