| | package handler
|
| |
|
| | import (
|
| | "bytes"
|
| | "encoding/json"
|
| | "fmt"
|
| | "io"
|
| | "log"
|
| | "net/http"
|
| | "opus-api/internal/converter"
|
| | "opus-api/internal/logger"
|
| | "opus-api/internal/model"
|
| | "opus-api/internal/stream"
|
| | "opus-api/internal/tokenizer"
|
| | "opus-api/internal/types"
|
| | "strings"
|
| |
|
| | "github.com/gin-gonic/gin"
|
| | "github.com/google/uuid"
|
| | )
|
| |
|
| |
|
| | func HandleMessages(c *gin.Context) {
|
| |
|
| | requestID := uuid.New().String()[:8]
|
| |
|
| |
|
| | if types.DebugMode {
|
| | logger.RotateLogs()
|
| | }
|
| |
|
| |
|
| | var claudeReq types.ClaudeRequest
|
| | if err := c.ShouldBindJSON(&claudeReq); err != nil {
|
| | c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
|
| | return
|
| | }
|
| |
|
| |
|
| | if claudeReq.Model == "" {
|
| | claudeReq.Model = types.DefaultModel
|
| | }
|
| | if !types.IsModelSupported(claudeReq.Model) {
|
| | c.JSON(http.StatusBadRequest, gin.H{
|
| | "error": fmt.Sprintf("Model '%s' is not supported. Supported models: %v", claudeReq.Model, types.SupportedModels),
|
| | })
|
| | return
|
| | }
|
| |
|
| |
|
| | var logFolder string
|
| | if types.DebugMode {
|
| | logFolder, _ = logger.CreateLogFolder(requestID)
|
| | logger.WriteJSONLog(logFolder, "1_claude_request.json", claudeReq)
|
| | }
|
| |
|
| |
|
| | morphReq := converter.ClaudeToMorph(claudeReq)
|
| |
|
| |
|
| | if types.DebugMode && logFolder != "" {
|
| | logger.WriteJSONLog(logFolder, "2_morph_request.json", morphReq)
|
| | }
|
| |
|
| |
|
| | morphReqJSON, err := json.Marshal(morphReq)
|
| | if err != nil {
|
| | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal request"})
|
| | return
|
| | }
|
| |
|
| | req, err := http.NewRequest("POST", types.MorphAPIURL, bytes.NewReader(morphReqJSON))
|
| | if err != nil {
|
| | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
|
| | return
|
| | }
|
| |
|
| |
|
| | headers := make(map[string]string)
|
| | for key, value := range types.MorphHeaders {
|
| | headers[key] = value
|
| | }
|
| |
|
| |
|
| | if types.CookieRotatorInstance != nil {
|
| | cookieInterface, err := types.CookieRotatorInstance.NextCookie()
|
| | if err == nil && cookieInterface != nil {
|
| |
|
| | if cookie, ok := cookieInterface.(*model.MorphCookie); ok {
|
| | headers["cookie"] = cookie.APIKey
|
| | log.Printf("[INFO] Using rotated cookie (ID: %d, Priority: %d)", cookie.ID, cookie.Priority)
|
| | } else {
|
| | log.Printf("[WARN] Cookie type assertion failed, using default")
|
| | }
|
| | } else {
|
| | log.Printf("[WARN] Failed to get rotated cookie: %v, using default", err)
|
| | }
|
| | }
|
| |
|
| |
|
| | for key, value := range headers {
|
| | req.Header.Set(key, value)
|
| | }
|
| |
|
| |
|
| | if types.DebugMode && logFolder != "" {
|
| | var reqLog strings.Builder
|
| | reqLog.WriteString(fmt.Sprintf("%s %s\n", req.Method, req.URL))
|
| | for k, v := range req.Header {
|
| | reqLog.WriteString(fmt.Sprintf("%s: %s\n", k, strings.Join(v, ", ")))
|
| | }
|
| | reqLog.WriteString("\n")
|
| | reqLog.Write(morphReqJSON)
|
| | logger.WriteTextLog(logFolder, "3_upstream_request.txt", reqLog.String())
|
| | }
|
| |
|
| |
|
| | client := &http.Client{}
|
| | resp, err := client.Do(req)
|
| | if err != nil {
|
| | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to upstream API"})
|
| | return
|
| | }
|
| | defer resp.Body.Close()
|
| |
|
| | if resp.StatusCode != http.StatusOK {
|
| | bodyBytes, _ := io.ReadAll(resp.Body)
|
| | if types.DebugMode && logFolder != "" {
|
| | logger.WriteTextLog(logFolder, "error.txt", fmt.Sprintf("Error: %d %s\n%s", resp.StatusCode, resp.Status, string(bodyBytes)))
|
| | }
|
| | c.JSON(http.StatusInternalServerError, gin.H{
|
| | "error": "Failed to connect to upstream API",
|
| | "status": resp.StatusCode,
|
| | })
|
| | return
|
| | }
|
| |
|
| |
|
| | c.Header("Content-Type", "text/event-stream")
|
| | c.Header("Cache-Control", "no-cache")
|
| | c.Header("Connection", "keep-alive")
|
| |
|
| |
|
| | var clientResponseWriter io.Writer = io.Discard
|
| | if types.DebugMode && logFolder != "" {
|
| | logger.WriteTextLog(logFolder, "5_client_response.txt", "")
|
| | clientResponseWriter = &logWriter{logFolder: logFolder, fileName: "5_client_response.txt"}
|
| | }
|
| | onChunk := func(chunk string) {
|
| | if types.DebugMode {
|
| | clientResponseWriter.Write([]byte(chunk))
|
| | }
|
| | }
|
| |
|
| |
|
| | inputTokens := calculateInputTokens(claudeReq)
|
| |
|
| |
|
| | pr, pw := io.Pipe()
|
| |
|
| |
|
| | go func() {
|
| | defer pw.Close()
|
| |
|
| |
|
| | var morphResponseWriter io.Writer = io.Discard
|
| | if types.DebugMode && logFolder != "" {
|
| | logger.WriteTextLog(logFolder, "4_upstream_response.txt", "")
|
| | morphResponseWriter = &logWriter{logFolder: logFolder, fileName: "4_upstream_response.txt"}
|
| | }
|
| |
|
| |
|
| | teeReader := io.TeeReader(resp.Body, morphResponseWriter)
|
| |
|
| |
|
| | if err := stream.TransformMorphToClaudeStream(teeReader, claudeReq.Model, inputTokens, pw, onChunk); err != nil {
|
| | log.Printf("[ERROR] Stream transformation error: %v", err)
|
| | }
|
| | }()
|
| |
|
| |
|
| | c.Stream(func(w io.Writer) bool {
|
| | buf := make([]byte, 4096)
|
| | n, err := pr.Read(buf)
|
| | if n > 0 {
|
| | w.Write(buf[:n])
|
| | }
|
| | return err == nil
|
| | })
|
| | }
|
| |
|
| |
|
| | type logWriter struct {
|
| | logFolder string
|
| | fileName string
|
| | }
|
| |
|
| | func (w *logWriter) Write(p []byte) (n int, err error) {
|
| | if types.DebugMode && w.logFolder != "" {
|
| | logger.AppendLog(w.logFolder, w.fileName, string(p))
|
| | }
|
| | return len(p), nil
|
| | }
|
| |
|
| |
|
| | func calculateInputTokens(req types.ClaudeRequest) int {
|
| | var totalText strings.Builder
|
| |
|
| |
|
| | if req.System != nil {
|
| | if sysStr, ok := req.System.(string); ok {
|
| | totalText.WriteString(sysStr)
|
| | } else if sysList, ok := req.System.([]interface{}); ok {
|
| | for _, item := range sysList {
|
| | if itemMap, ok := item.(map[string]interface{}); ok {
|
| | if text, ok := itemMap["text"].(string); ok {
|
| | totalText.WriteString(text)
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| | for _, msg := range req.Messages {
|
| | if content, ok := msg.Content.(string); ok {
|
| | totalText.WriteString(content)
|
| | } else if contentBlocks, ok := msg.Content.([]types.ClaudeContentBlock); ok {
|
| | for _, block := range contentBlocks {
|
| | if textBlock, ok := block.(types.ClaudeContentBlockText); ok {
|
| | totalText.WriteString(textBlock.Text)
|
| | } else if toolResult, ok := block.(types.ClaudeContentBlockToolResult); ok {
|
| | if resultStr, ok := toolResult.Content.(string); ok {
|
| | totalText.WriteString(resultStr)
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| | for _, tool := range req.Tools {
|
| | totalText.WriteString(tool.Name)
|
| | totalText.WriteString(tool.Description)
|
| | if tool.InputSchema != nil {
|
| | schemaBytes, _ := json.Marshal(tool.InputSchema)
|
| | totalText.Write(schemaBytes)
|
| | }
|
| | }
|
| |
|
| | return tokenizer.CountTokens(totalText.String())
|
| | } |