| 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())
|
| } |