Fixed json format and endstream

This commit is contained in:
Ilya Sharov
2025-04-11 00:33:49 +03:00
parent c1e465c698
commit 0eb365f44c

138
main.go
View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -136,6 +137,7 @@ func main() {
var request struct { var request struct {
Model string `json:"model"` Model string `json:"model"`
Messages []openai.ChatCompletionMessage `json:"messages"` Messages []openai.ChatCompletionMessage `json:"messages"`
Stream *bool `json:"stream"` // Добавим поле Stream
} }
// Parse the JSON request // Parse the JSON request
@@ -144,10 +146,28 @@ func main() {
return return
} }
// Определяем, нужен ли стриминг (по умолчанию true, если не указано для /api/chat)
// ВАЖНО: Open WebUI может НЕ передавать "stream": true для /api/chat, подразумевая это.
// Нужно проверить, какой запрос шлет Open WebUI. Если не шлет, ставим true.
streamRequested := true
if request.Stream != nil {
streamRequested = *request.Stream
}
// Если стриминг не запрошен, нужно будет реализовать отдельную логику
// для сбора полного ответа и отправки его одним JSON.
// Пока реализуем только стриминг.
if !streamRequested {
// TODO: Реализовать не-потоковый ответ, если нужно
c.JSON(http.StatusNotImplemented, gin.H{"error": "Non-streaming response not implemented yet"})
return
}
fullModelName, err := provider.GetFullModelName(request.Model) fullModelName, err := provider.GetFullModelName(request.Model)
if err != nil { if err != nil {
slog.Error("Error getting full model name", "Error", err) slog.Error("Error getting full model name", "Error", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // Ollama возвращает 404 на неправильное имя модели
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return return
} }
@@ -160,72 +180,108 @@ func main() {
} }
defer stream.Close() // Ensure stream closure defer stream.Close() // Ensure stream closure
// Set headers for streaming response // --- ИСПРАВЛЕНИЯ для NDJSON (Ollama-style) ---
c.Writer.Header().Set("Content-Type", "application/json")
c.Writer.Header().Set("Transfer-Encoding", "chunked") // Set headers CORRECTLY for Newline Delimited JSON
c.Status(http.StatusOK) c.Writer.Header().Set("Content-Type", "application/x-ndjson") // <--- КЛЮЧЕВОЕ ИЗМЕНЕНИЕ
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
// Transfer-Encoding: chunked устанавливается Gin автоматически
w := c.Writer // Получаем ResponseWriter
flusher, ok := w.(http.Flusher)
if !ok {
slog.Error("Expected http.ResponseWriter to be an http.Flusher")
// Отправить ошибку клиенту уже сложно, т.к. заголовки могли уйти
return
}
var lastFinishReason string
// Stream responses back to the client // Stream responses back to the client
for { for {
response, err := stream.Recv() response, err := stream.Recv()
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
// End of stream // End of stream from the backend provider
break break
} }
if err != nil { if err != nil {
// Handle errors slog.Error("Backend stream error", "Error", err)
slog.Error("Stream error", "Error", err) // Попытка отправить ошибку в формате NDJSON
c.Status(http.StatusInternalServerError) // Ollama обычно просто обрывает соединение или шлет 500 перед этим
c.Writer.Write([]byte("Error streaming: " + err.Error() + "\n")) errorMsg := map[string]string{"error": "Stream error: " + err.Error()}
c.Writer.Flush() errorJson, _ := json.Marshal(errorMsg)
fmt.Fprintf(w, "%s\n", string(errorJson)) // Отправляем ошибку + \n
flusher.Flush()
return return
} }
// Build JSON response structure // Сохраняем причину остановки, если она есть в чанке
if len(response.Choices) > 0 && response.Choices[0].FinishReason != "" {
lastFinishReason = string(response.Choices[0].FinishReason)
}
// Build JSON response structure for intermediate chunks (Ollama chat format)
responseJSON := map[string]interface{}{ responseJSON := map[string]interface{}{
"model": fullModelName, "model": fullModelName,
"created_at": time.Now().Format(time.RFC3339), "created_at": time.Now().Format(time.RFC3339),
"message": map[string]string{ "message": map[string]string{
"role": "assistant", "role": "assistant",
"content": response.Choices[0].Delta.Content, "content": response.Choices[0].Delta.Content, // Может быть ""
}, },
"done": false, "done": false, // Всегда false для промежуточных чанков
}
// Marshal JSON
jsonData, err := json.Marshal(responseJSON)
if err != nil {
slog.Error("Error marshaling intermediate response JSON", "Error", err)
return // Прерываем, так как не можем отправить данные
}
// Send JSON object followed by a newline
fmt.Fprintf(w, "%s\n", string(jsonData)) // <--- ИЗМЕНЕНО: Формат NDJSON (JSON + \n)
// Flush data to send it immediately
flusher.Flush()
}
// --- Отправка финального сообщения (done: true) в стиле Ollama ---
// Определяем причину остановки (если бэкенд не дал, ставим 'stop')
// Ollama использует 'stop', 'length', 'content_filter', 'tool_calls'
if lastFinishReason == "" {
lastFinishReason = "stop"
}
// ВАЖНО: Замените nil на 0 для числовых полей статистики
finalResponse := map[string]interface{}{
"model": fullModelName,
"created_at": time.Now().Format(time.RFC3339),
"done": true,
"finish_reason": lastFinishReason, // Необязательно для /api/chat Ollama, но не вредит
"total_duration": 0, "total_duration": 0,
"load_duration": 0, "load_duration": 0,
"prompt_eval_count": nil, // Replace with actual prompt tokens if available "prompt_eval_count": 0, // <--- ИЗМЕНЕНО: nil заменен на 0
"eval_count": nil, // Replace with actual completion tokens if available "eval_count": 0, // <--- ИЗМЕНЕНО: nil заменен на 0
"eval_duration": 0, "eval_duration": 0,
} }
// Marshal and send the JSON response finalJsonData, err := json.Marshal(finalResponse)
if err := json.NewEncoder(c.Writer).Encode(responseJSON); err != nil { if err != nil {
slog.Error("Error encoding response", "Error", err) slog.Error("Error marshaling final response JSON", "Error", err)
c.Status(http.StatusInternalServerError)
return return
} }
// Flush data to send it immediately // Отправляем финальный JSON-объект + newline
c.Writer.Flush() fmt.Fprintf(w, "%s\n", string(finalJsonData)) // <--- ИЗМЕНЕНО: Формат NDJSON
} flusher.Flush()
// Final response indicating the stream has ended // ВАЖНО: Для NDJSON НЕТ 'data: [DONE]' маркера.
endResponse := map[string]interface{}{ // Клиент понимает конец потока по получению объекта с "done": true
"model": fullModelName, // и/или по закрытию соединения сервером (что Gin сделает автоматически после выхода из хендлера).
"created_at": time.Now().Format(time.RFC3339),
"message": map[string]string{ // --- Конец исправлений ---
"role": "assistant",
"content": "",
},
"done": true,
"total_duration": 0,
"load_duration": 0,
"prompt_eval_count": nil,
"eval_count": nil,
"eval_duration": 0,
}
if err := json.NewEncoder(c.Writer).Encode(endResponse); err != nil {
slog.Error("Error encoding end response", "Error", err)
}
}) })
r.Run(":11434") r.Run(":11434")