package middleware import ( "bytes" "encoding/json" "fmt" "io" "net/http" "github.com/gin-gonic/gin" "github.com/ollama/ollama/anthropic" "github.com/ollama/ollama/api" ) // AnthropicWriter wraps the response writer to transform Ollama responses to Anthropic format type AnthropicWriter struct { BaseWriter stream bool id string model string converter *anthropic.StreamConverter } func (w *AnthropicWriter) writeError(data []byte) (int, error) { var errData struct { Error string `json:"error"` } if err := json.Unmarshal(data, &errData); err != nil { return 0, err } w.ResponseWriter.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w.ResponseWriter).Encode(anthropic.NewError(w.ResponseWriter.Status(), errData.Error)) if err != nil { return 0, err } return len(data), nil } func (w *AnthropicWriter) writeEvent(eventType string, data any) error { d, err := json.Marshal(data) if err != nil { return err } _, err = w.ResponseWriter.Write([]byte(fmt.Sprintf("event: %s\ndata: %s\n\n", eventType, d))) if err != nil { return err } if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } return nil } func (w *AnthropicWriter) writeResponse(data []byte) (int, error) { var chatResponse api.ChatResponse err := json.Unmarshal(data, &chatResponse) if err != nil { return 0, err } if w.stream { w.ResponseWriter.Header().Set("Content-Type", "text/event-stream") events := w.converter.Process(chatResponse) for _, event := range events { if err := w.writeEvent(event.Event, event.Data); err != nil { return 0, err } } return len(data), nil } w.ResponseWriter.Header().Set("Content-Type", "application/json") response := anthropic.ToMessagesResponse(w.id, chatResponse) return len(data), json.NewEncoder(w.ResponseWriter).Encode(response) } func (w *AnthropicWriter) Write(data []byte) (int, error) { code := w.ResponseWriter.Status() if code != http.StatusOK { return w.writeError(data) } return w.writeResponse(data) } // AnthropicMessagesMiddleware handles Anthropic Messages API requests func AnthropicMessagesMiddleware() gin.HandlerFunc { return func(c *gin.Context) { var req anthropic.MessagesRequest err := c.ShouldBindJSON(&req) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, anthropic.NewError(http.StatusBadRequest, err.Error())) return } if req.Model == "" { c.AbortWithStatusJSON(http.StatusBadRequest, anthropic.NewError(http.StatusBadRequest, "model is required")) return } if req.MaxTokens <= 0 { c.AbortWithStatusJSON(http.StatusBadRequest, anthropic.NewError(http.StatusBadRequest, "max_tokens is required and must be positive")) return } if len(req.Messages) == 0 { c.AbortWithStatusJSON(http.StatusBadRequest, anthropic.NewError(http.StatusBadRequest, "messages is required")) return } chatReq, err := anthropic.FromMessagesRequest(req) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, anthropic.NewError(http.StatusBadRequest, err.Error())) return } var b bytes.Buffer if err := json.NewEncoder(&b).Encode(chatReq); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, anthropic.NewError(http.StatusInternalServerError, err.Error())) return } c.Request.Body = io.NopCloser(&b) messageID := anthropic.GenerateMessageID() w := &AnthropicWriter{ BaseWriter: BaseWriter{ResponseWriter: c.Writer}, stream: req.Stream, id: messageID, model: req.Model, converter: anthropic.NewStreamConverter(messageID, req.Model), } if req.Stream { c.Writer.Header().Set("Content-Type", "text/event-stream") c.Writer.Header().Set("Cache-Control", "no-cache") c.Writer.Header().Set("Connection", "keep-alive") } c.Writer = w c.Next() } }