Spaces:
Paused
Paused
Update handlers.go
Browse files- handlers.go +76 -64
handlers.go
CHANGED
|
@@ -9,6 +9,7 @@ import (
|
|
| 9 |
"net"
|
| 10 |
"net/http"
|
| 11 |
"net/url"
|
|
|
|
| 12 |
"strings"
|
| 13 |
"time"
|
| 14 |
|
|
@@ -16,42 +17,52 @@ import (
|
|
| 16 |
"github.com/google/uuid"
|
| 17 |
)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
func maskURL(rawURL string) string {
|
| 20 |
u, err := url.Parse(rawURL)
|
| 21 |
if err != nil {
|
| 22 |
-
return rawURL
|
| 23 |
}
|
| 24 |
|
| 25 |
originalHostname := u.Hostname()
|
| 26 |
if originalHostname == "" {
|
| 27 |
-
return rawURL
|
| 28 |
}
|
| 29 |
|
|
|
|
| 30 |
if net.ParseIP(originalHostname) != nil {
|
| 31 |
return rawURL
|
| 32 |
}
|
| 33 |
|
| 34 |
hostParts := strings.Split(originalHostname, ".")
|
| 35 |
maskedHostname := ""
|
| 36 |
-
prefixToKeepLength := 3
|
| 37 |
|
| 38 |
-
if len(hostParts) == 1 {
|
| 39 |
nameOnly := hostParts[0]
|
| 40 |
prefix := nameOnly
|
| 41 |
if len(nameOnly) > prefixToKeepLength {
|
| 42 |
prefix = nameOnly[:prefixToKeepLength]
|
| 43 |
}
|
| 44 |
maskedHostname = prefix + "******"
|
| 45 |
-
} else if len(hostParts) >= 2 {
|
| 46 |
-
tld := hostParts[len(hostParts)-1]
|
| 47 |
-
namePart := strings.Join(hostParts[:len(hostParts)-1], ".")
|
| 48 |
|
| 49 |
prefix := namePart
|
| 50 |
if len(namePart) > prefixToKeepLength {
|
| 51 |
prefix = namePart[:prefixToKeepLength]
|
| 52 |
}
|
| 53 |
maskedHostname = prefix + "******" + "." + tld
|
| 54 |
-
} else {
|
| 55 |
maskedHostname = "******"
|
| 56 |
}
|
| 57 |
|
|
@@ -60,33 +71,35 @@ func maskURL(rawURL string) string {
|
|
| 60 |
newHost += ":" + u.Port()
|
| 61 |
}
|
| 62 |
|
| 63 |
-
maskedU := *u
|
| 64 |
maskedU.Host = newHost
|
| 65 |
|
| 66 |
return maskedU.String()
|
| 67 |
}
|
| 68 |
|
|
|
|
| 69 |
func HealthCheckHandler(c *gin.Context) {
|
| 70 |
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
|
| 71 |
}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
| 73 |
func MessagesHandler(c *gin.Context) {
|
| 74 |
-
requestID := fmt.Sprintf("msg_%s", uuid.NewString()[:24])
|
| 75 |
-
cfg := LoadConfig()
|
| 76 |
startTime := time.Now()
|
| 77 |
|
| 78 |
var claudeReq ClaudeRequest
|
| 79 |
bodyBytes, err := io.ReadAll(c.Request.Body)
|
| 80 |
if err != nil {
|
| 81 |
-
// Giữ lại: Log lỗi quan trọng
|
| 82 |
log.Printf("ERROR: [%s] Không thể đọc nội dung yêu cầu: %v", requestID, err)
|
| 83 |
sendClaudeError(c, http.StatusBadRequest, "invalid_request_error", "Không thể đọc nội dung yêu cầu.")
|
| 84 |
return
|
| 85 |
}
|
| 86 |
-
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) //
|
| 87 |
|
| 88 |
if err := json.Unmarshal(bodyBytes, &claudeReq); err != nil {
|
| 89 |
-
// Giữ lại: Log lỗi quan trọng
|
| 90 |
log.Printf("ERROR: [%s] Không thể giải mã JSON yêu cầu: %v. Body: %s", requestID, err, string(bodyBytes))
|
| 91 |
sendClaudeError(c, http.StatusBadRequest, "invalid_request_error", "Định dạng JSON không hợp lệ trong nội dung yêu cầu.")
|
| 92 |
return
|
|
@@ -95,80 +108,89 @@ func MessagesHandler(c *gin.Context) {
|
|
| 95 |
isStreaming := claudeReq.Stream
|
| 96 |
modelRequested := claudeReq.Model
|
| 97 |
if modelRequested == "" {
|
| 98 |
-
modelRequested = "unknown_model"
|
| 99 |
}
|
| 100 |
-
// Giữ lại: Log INFO quan trọng về yêu cầu nhận được
|
| 101 |
log.Printf("INFO: [%s] Nhận yêu cầu. Stream: %t, Model: %s, ClientIP: %s, NativeClaude: %t",
|
| 102 |
requestID, isStreaming, modelRequested, c.ClientIP(), cfg.UpstreamNativeClaudeFormat)
|
| 103 |
|
| 104 |
var upstreamURL string
|
| 105 |
var upstreamRequestBodyBytes []byte
|
| 106 |
-
var openAIReqForLogging *OpenAIRequest //
|
| 107 |
|
| 108 |
if cfg.UpstreamNativeClaudeFormat {
|
| 109 |
-
// Upstream
|
| 110 |
-
upstreamURL = strings.TrimRight(cfg.OpenAIAPIEndpoint, "/") + "/v1/messages" //
|
| 111 |
-
upstreamRequestBodyBytes = bodyBytes //
|
| 112 |
-
// Giữ lại: Log INFO
|
| 113 |
log.Printf("INFO: [%s] Upstream là Claude native. Chuyển tiếp yêu cầu gốc đến %s", requestID, maskURL(upstreamURL))
|
| 114 |
} else {
|
| 115 |
-
// Upstream
|
| 116 |
-
upstreamURL = strings.TrimRight(cfg.OpenAIAPIEndpoint, "/") + "/v1/chat/completions" //
|
| 117 |
openAIReq, errConv := convertClaudeRequestToOpenAI(&claudeReq)
|
| 118 |
if errConv != nil {
|
| 119 |
-
// Giữ lại: Log lỗi quan trọng
|
| 120 |
log.Printf("ERROR: [%s] Không thể chuyển đổi yêu cầu Claude sang OpenAI: %v", requestID, errConv)
|
| 121 |
sendClaudeError(c, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("Lỗi chuyển đổi dữ liệu yêu cầu: %v", errConv))
|
| 122 |
return
|
| 123 |
}
|
| 124 |
-
openAIReq.Stream = isStreaming //
|
| 125 |
-
openAIReqForLogging = openAIReq //
|
| 126 |
|
| 127 |
openaiReqBytesMarshal, errMarshal := json.Marshal(openAIReq)
|
| 128 |
if errMarshal != nil {
|
| 129 |
-
// Giữ lại: Log lỗi quan trọng
|
| 130 |
log.Printf("ERROR: [%s] Không thể marshal yêu cầu OpenAI JSON: %v", requestID, errMarshal)
|
| 131 |
sendClaudeError(c, http.StatusInternalServerError, "internal_server_error", "Không thể chuẩn bị yêu cầu upstream.")
|
| 132 |
return
|
| 133 |
}
|
| 134 |
upstreamRequestBodyBytes = openaiReqBytesMarshal
|
| 135 |
-
// Giữ lại: Log INFO
|
| 136 |
log.Printf("INFO: [%s] Upstream là OpenAI. Chuyển đổi và gửi yêu cầu đến %s", requestID, maskURL(upstreamURL))
|
| 137 |
}
|
| 138 |
|
|
|
|
| 139 |
req, err := http.NewRequestWithContext(c.Request.Context(), "POST", upstreamURL, bytes.NewBuffer(upstreamRequestBodyBytes))
|
| 140 |
if err != nil {
|
| 141 |
-
// Giữ lại: Log lỗi quan trọng
|
| 142 |
log.Printf("ERROR: [%s] Không thể tạo yêu cầu HTTP upstream: %v", requestID, err)
|
| 143 |
sendClaudeError(c, http.StatusInternalServerError, "internal_server_error", "Không thể tạo yêu cầu upstream.")
|
| 144 |
return
|
| 145 |
}
|
| 146 |
|
|
|
|
| 147 |
req.Header.Set("Content-Type", "application/json")
|
| 148 |
if isStreaming {
|
| 149 |
req.Header.Set("Accept", "text/event-stream")
|
| 150 |
} else {
|
| 151 |
req.Header.Set("Accept", "application/json")
|
| 152 |
}
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
-
// Giữ lại: Log INFO quan trọng về việc gửi yêu cầu upstream
|
| 163 |
log.Printf("INFO: [%s] Gửi yêu cầu upstream (Stream=%t) đến %s...", requestID, isStreaming, maskURL(upstreamURL))
|
| 164 |
|
|
|
|
| 165 |
httpClient := &http.Client{
|
| 166 |
-
Transport: cfg.UpstreamTransport,
|
| 167 |
-
Timeout: 0,
|
| 168 |
}
|
| 169 |
upstreamResp, err := httpClient.Do(req)
|
| 170 |
if err != nil {
|
| 171 |
-
// Giữ lại: Log lỗi quan trọng
|
| 172 |
log.Printf("ERROR: [%s] Yêu cầu upstream thất bại: %v", requestID, err)
|
| 173 |
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
| 174 |
sendClaudeError(c, http.StatusGatewayTimeout, "api_error", fmt.Sprintf("Gateway Timeout khi kết nối upstream (%v).", cfg.ConnectTimeout))
|
|
@@ -177,19 +199,17 @@ func MessagesHandler(c *gin.Context) {
|
|
| 177 |
}
|
| 178 |
return
|
| 179 |
}
|
| 180 |
-
// Giữ lại: Log INFO quan trọng về trạng thái upstream
|
| 181 |
log.Printf("INFO: [%s] Nhận trạng thái upstream: %d (%s)", requestID, upstreamResp.StatusCode, http.StatusText(upstreamResp.StatusCode))
|
| 182 |
|
|
|
|
| 183 |
if upstreamResp.StatusCode != http.StatusOK {
|
| 184 |
errorBodyBytes, readErr := io.ReadAll(upstreamResp.Body)
|
| 185 |
if readErr != nil {
|
| 186 |
-
// Giữ lại: Log cảnh báo
|
| 187 |
log.Printf("WARN: [%s] Không thể đọc nội dung lỗi upstream (Status %d): %v", requestID, upstreamResp.StatusCode, readErr)
|
| 188 |
}
|
| 189 |
-
_ = upstreamResp.Body.Close()
|
| 190 |
|
| 191 |
errorBodyStr := string(errorBodyBytes)
|
| 192 |
-
// Giữ lại: Log lỗi quan trọng
|
| 193 |
log.Printf("ERROR: [%s] Upstream trả về lỗi %d. Chi tiết ngắn: %s", requestID, upstreamResp.StatusCode, truncateString(strings.TrimSpace(errorBodyStr), 100))
|
| 194 |
|
| 195 |
var errorType string
|
|
@@ -210,54 +230,47 @@ func MessagesHandler(c *gin.Context) {
|
|
| 210 |
return
|
| 211 |
}
|
| 212 |
|
|
|
|
| 213 |
if isStreaming {
|
| 214 |
if cfg.UpstreamNativeClaudeFormat {
|
| 215 |
-
// Giữ lại: Log INFO quan trọng
|
| 216 |
log.Printf("INFO: [%s] Upstream Claude native stream nhận được. Bắt đầu proxy SSE (với hack usage).", requestID)
|
| 217 |
-
proxyClaudeNativeSSE(c, upstreamResp, requestID, &claudeReq)
|
| 218 |
} else {
|
| 219 |
-
// Giữ lại: Log INFO quan trọng
|
| 220 |
log.Printf("INFO: [%s] Upstream OpenAI stream nhận được. Bắt đầu chuyển đổi SSE.", requestID)
|
| 221 |
-
modelForConversion := claudeReq.Model
|
| 222 |
if openAIReqForLogging != nil && openAIReqForLogging.Model != "" {
|
| 223 |
-
modelForConversion = openAIReqForLogging.Model
|
| 224 |
}
|
| 225 |
streamOpenAIResponseToClaudeSSE(c, upstreamResp, requestID, modelForConversion, &claudeReq)
|
| 226 |
}
|
| 227 |
} else { // Non-streaming
|
| 228 |
-
// Giữ lại: Log INFO quan trọng
|
| 229 |
log.Printf("INFO: [%s] Phản hồi non-stream upstream nhận được. Đang xử lý.", requestID)
|
| 230 |
-
defer upstreamResp.Body.Close()
|
| 231 |
|
| 232 |
respBodyBytes, errRead := io.ReadAll(upstreamResp.Body)
|
| 233 |
if errRead != nil {
|
| 234 |
-
// Giữ lại: Log lỗi quan trọng
|
| 235 |
log.Printf("ERROR: [%s] Không thể đọc nội dung phản hồi non-streaming upstream: %v", requestID, errRead)
|
| 236 |
sendClaudeError(c, http.StatusBadGateway, "api_error", "Không thể đọc phản hồi upstream.")
|
| 237 |
return
|
| 238 |
}
|
| 239 |
|
| 240 |
if cfg.UpstreamNativeClaudeFormat {
|
| 241 |
-
// Giữ lại: Log INFO
|
| 242 |
log.Printf("INFO: [%s] Upstream Claude native non-stream response nhận được. Chuyển tiếp trực tiếp.", requestID)
|
| 243 |
-
var tempClaudeResp map[string]interface{}
|
| 244 |
if errJson := json.Unmarshal(respBodyBytes, &tempClaudeResp); errJson != nil {
|
| 245 |
-
// Giữ lại: Log lỗi quan trọng
|
| 246 |
log.Printf("ERROR: [%s] Upstream Claude native response không phải JSON hợp lệ: %v. Body: %s", requestID, errJson, string(respBodyBytes))
|
| 247 |
sendClaudeError(c, http.StatusBadGateway, "api_error", "API upstream (Claude native) trả về JSON không hợp lệ.")
|
| 248 |
return
|
| 249 |
}
|
| 250 |
clientContentType := upstreamResp.Header.Get("Content-Type")
|
| 251 |
if clientContentType == "" {
|
| 252 |
-
clientContentType = "application/json; charset=utf-8"
|
| 253 |
}
|
| 254 |
-
c.Data(http.StatusOK, clientContentType, respBodyBytes)
|
| 255 |
} else {
|
| 256 |
-
// Giữ lại: Log INFO
|
| 257 |
log.Printf("INFO: [%s] Phản hồi non-stream OpenAI upstream nhận được. Đang chuyển đổi.", requestID)
|
| 258 |
var openAIResp OpenAIResponse
|
| 259 |
if errJson := json.Unmarshal(respBodyBytes, &openAIResp); errJson != nil {
|
| 260 |
-
// Giữ lại: Log lỗi quan trọng
|
| 261 |
log.Printf("ERROR: [%s] Không thể giải mã JSON non-streaming OpenAI upstream: %v. Body: %s", requestID, errJson, string(respBodyBytes))
|
| 262 |
sendClaudeError(c, http.StatusBadGateway, "api_error", "API upstream (OpenAI) trả về JSON không hợp lệ.")
|
| 263 |
return
|
|
@@ -265,18 +278,17 @@ func MessagesHandler(c *gin.Context) {
|
|
| 265 |
|
| 266 |
claudeRespToClient, errConv := convertOpenAIResponseToClaude(&openAIResp, requestID)
|
| 267 |
if errConv != nil {
|
| 268 |
-
// Giữ lại: Log lỗi quan trọng
|
| 269 |
log.Printf("ERROR: [%s] Không thể chuyển đổi phản hồi non-streaming OpenAI: %v", requestID, errConv)
|
| 270 |
sendClaudeError(c, http.StatusInternalServerError, "internal_server_error", fmt.Sprintf("Lỗi xử lý phản hồi upstream: %v", errConv))
|
| 271 |
return
|
| 272 |
}
|
| 273 |
c.JSON(http.StatusOK, claudeRespToClient)
|
| 274 |
}
|
| 275 |
-
// Giữ lại: Log INFO hữu ích về thời gian xử lý
|
| 276 |
log.Printf("INFO: [%s] Xử lý thành công yêu cầu non-streaming trong %v", requestID, time.Since(startTime))
|
| 277 |
}
|
| 278 |
}
|
| 279 |
|
|
|
|
| 280 |
func sendClaudeError(c *gin.Context, statusCode int, errorType string, message string) {
|
| 281 |
errResp := ClaudeErrorResponse{
|
| 282 |
Type: "error",
|
|
@@ -285,14 +297,14 @@ func sendClaudeError(c *gin.Context, statusCode int, errorType string, message s
|
|
| 285 |
Message: message,
|
| 286 |
},
|
| 287 |
}
|
| 288 |
-
if statusCode < 400 || statusCode > 599 {
|
| 289 |
-
// Giữ lại: Log cảnh báo về mã trạng thái không hợp lệ
|
| 290 |
log.Printf("WARN: Mã trạng thái không hợp lệ %d được cung cấp cho lỗi, mặc định thành 500.", statusCode)
|
| 291 |
statusCode = http.StatusInternalServerError
|
| 292 |
}
|
| 293 |
-
c.AbortWithStatusJSON(statusCode, errResp)
|
| 294 |
}
|
| 295 |
|
|
|
|
| 296 |
func truncateString(s string, maxLength int) string {
|
| 297 |
if len(s) <= maxLength {
|
| 298 |
return s
|
|
|
|
| 9 |
"net"
|
| 10 |
"net/http"
|
| 11 |
"net/url"
|
| 12 |
+
"os" // Thêm os để sử dụng os.Getenv
|
| 13 |
"strings"
|
| 14 |
"time"
|
| 15 |
|
|
|
|
| 17 |
"github.com/google/uuid"
|
| 18 |
)
|
| 19 |
|
| 20 |
+
const (
|
| 21 |
+
// ANTHROPIC_VERSION_ENV_VAR là tên biến môi trường để lấy phiên bản Anthropic
|
| 22 |
+
ANTHROPIC_VERSION_ENV_VAR = "ANTHROPIC_VERSION"
|
| 23 |
+
// DEFAULT_ANTHROPIC_VERSION là phiên bản Anthropic mặc định nếu không được đặt trong biến môi trường
|
| 24 |
+
DEFAULT_ANTHROPIC_VERSION = "2023-06-01"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
// maskURL che giấu hostname trong URL để bảo mật log.
|
| 28 |
+
// Ví dụ: "https://api.example.com/v1" -> "https://api******.com/v1"
|
| 29 |
func maskURL(rawURL string) string {
|
| 30 |
u, err := url.Parse(rawURL)
|
| 31 |
if err != nil {
|
| 32 |
+
return rawURL // Trả về URL gốc nếu không parse được
|
| 33 |
}
|
| 34 |
|
| 35 |
originalHostname := u.Hostname()
|
| 36 |
if originalHostname == "" {
|
| 37 |
+
return rawURL // Không có hostname để che
|
| 38 |
}
|
| 39 |
|
| 40 |
+
// Không che giấu địa chỉ IP
|
| 41 |
if net.ParseIP(originalHostname) != nil {
|
| 42 |
return rawURL
|
| 43 |
}
|
| 44 |
|
| 45 |
hostParts := strings.Split(originalHostname, ".")
|
| 46 |
maskedHostname := ""
|
| 47 |
+
prefixToKeepLength := 3 // Số ký tự đầu tiên của phần tên miền chính giữ lại
|
| 48 |
|
| 49 |
+
if len(hostParts) == 1 { // ví dụ: "localhost"
|
| 50 |
nameOnly := hostParts[0]
|
| 51 |
prefix := nameOnly
|
| 52 |
if len(nameOnly) > prefixToKeepLength {
|
| 53 |
prefix = nameOnly[:prefixToKeepLength]
|
| 54 |
}
|
| 55 |
maskedHostname = prefix + "******"
|
| 56 |
+
} else if len(hostParts) >= 2 { // ví dụ: "api.example.com"
|
| 57 |
+
tld := hostParts[len(hostParts)-1] // "com"
|
| 58 |
+
namePart := strings.Join(hostParts[:len(hostParts)-1], ".") // "api.example"
|
| 59 |
|
| 60 |
prefix := namePart
|
| 61 |
if len(namePart) > prefixToKeepLength {
|
| 62 |
prefix = namePart[:prefixToKeepLength]
|
| 63 |
}
|
| 64 |
maskedHostname = prefix + "******" + "." + tld
|
| 65 |
+
} else { // Trường hợp không mong muốn, che toàn bộ
|
| 66 |
maskedHostname = "******"
|
| 67 |
}
|
| 68 |
|
|
|
|
| 71 |
newHost += ":" + u.Port()
|
| 72 |
}
|
| 73 |
|
| 74 |
+
maskedU := *u // Tạo bản sao của URL
|
| 75 |
maskedU.Host = newHost
|
| 76 |
|
| 77 |
return maskedU.String()
|
| 78 |
}
|
| 79 |
|
| 80 |
+
// HealthCheckHandler xử lý kiểm tra tình trạng của proxy.
|
| 81 |
func HealthCheckHandler(c *gin.Context) {
|
| 82 |
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
|
| 83 |
}
|
| 84 |
|
| 85 |
+
// MessagesHandler xử lý các yêu cầu đến /v1/messages.
|
| 86 |
+
// Nó chuyển đổi yêu cầu từ định dạng Claude sang OpenAI (nếu cần),
|
| 87 |
+
// gửi đến upstream API, và chuyển đổi phản hồi trở lại định dạng Claude.
|
| 88 |
func MessagesHandler(c *gin.Context) {
|
| 89 |
+
requestID := fmt.Sprintf("msg_%s", uuid.NewString()[:24]) // Tạo ID duy nhất cho mỗi yêu cầu
|
| 90 |
+
cfg := LoadConfig() // Tải cấu hình (có thể tối ưu hóa để không tải mỗi lần)
|
| 91 |
startTime := time.Now()
|
| 92 |
|
| 93 |
var claudeReq ClaudeRequest
|
| 94 |
bodyBytes, err := io.ReadAll(c.Request.Body)
|
| 95 |
if err != nil {
|
|
|
|
| 96 |
log.Printf("ERROR: [%s] Không thể đọc nội dung yêu cầu: %v", requestID, err)
|
| 97 |
sendClaudeError(c, http.StatusBadRequest, "invalid_request_error", "Không thể đọc nội dung yêu cầu.")
|
| 98 |
return
|
| 99 |
}
|
| 100 |
+
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Khôi phục body để có thể đọc lại nếu cần
|
| 101 |
|
| 102 |
if err := json.Unmarshal(bodyBytes, &claudeReq); err != nil {
|
|
|
|
| 103 |
log.Printf("ERROR: [%s] Không thể giải mã JSON yêu cầu: %v. Body: %s", requestID, err, string(bodyBytes))
|
| 104 |
sendClaudeError(c, http.StatusBadRequest, "invalid_request_error", "Định dạng JSON không hợp lệ trong nội dung yêu cầu.")
|
| 105 |
return
|
|
|
|
| 108 |
isStreaming := claudeReq.Stream
|
| 109 |
modelRequested := claudeReq.Model
|
| 110 |
if modelRequested == "" {
|
| 111 |
+
modelRequested = "unknown_model" // Đặt tên model mặc định nếu client không cung cấp
|
| 112 |
}
|
|
|
|
| 113 |
log.Printf("INFO: [%s] Nhận yêu cầu. Stream: %t, Model: %s, ClientIP: %s, NativeClaude: %t",
|
| 114 |
requestID, isStreaming, modelRequested, c.ClientIP(), cfg.UpstreamNativeClaudeFormat)
|
| 115 |
|
| 116 |
var upstreamURL string
|
| 117 |
var upstreamRequestBodyBytes []byte
|
| 118 |
+
var openAIReqForLogging *OpenAIRequest // Chỉ dùng để log model nếu không phải native Claude
|
| 119 |
|
| 120 |
if cfg.UpstreamNativeClaudeFormat {
|
| 121 |
+
// Upstream API hỗ trợ định dạng Claude native
|
| 122 |
+
upstreamURL = strings.TrimRight(cfg.OpenAIAPIEndpoint, "/") + "/v1/messages" // Đường dẫn chuẩn của Claude API
|
| 123 |
+
upstreamRequestBodyBytes = bodyBytes // Sử dụng body gốc của Claude
|
|
|
|
| 124 |
log.Printf("INFO: [%s] Upstream là Claude native. Chuyển tiếp yêu cầu gốc đến %s", requestID, maskURL(upstreamURL))
|
| 125 |
} else {
|
| 126 |
+
// Upstream API là OpenAI hoặc tương thích OpenAI, cần chuyển đổi
|
| 127 |
+
upstreamURL = strings.TrimRight(cfg.OpenAIAPIEndpoint, "/") + "/v1/chat/completions" // Đường dẫn chuẩn của OpenAI API
|
| 128 |
openAIReq, errConv := convertClaudeRequestToOpenAI(&claudeReq)
|
| 129 |
if errConv != nil {
|
|
|
|
| 130 |
log.Printf("ERROR: [%s] Không thể chuyển đổi yêu cầu Claude sang OpenAI: %v", requestID, errConv)
|
| 131 |
sendClaudeError(c, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("Lỗi chuyển đổi dữ liệu yêu cầu: %v", errConv))
|
| 132 |
return
|
| 133 |
}
|
| 134 |
+
openAIReq.Stream = isStreaming // Đảm bảo cờ stream được đặt chính xác
|
| 135 |
+
openAIReqForLogging = openAIReq // Lưu lại để log model được sử dụng trong SSE
|
| 136 |
|
| 137 |
openaiReqBytesMarshal, errMarshal := json.Marshal(openAIReq)
|
| 138 |
if errMarshal != nil {
|
|
|
|
| 139 |
log.Printf("ERROR: [%s] Không thể marshal yêu cầu OpenAI JSON: %v", requestID, errMarshal)
|
| 140 |
sendClaudeError(c, http.StatusInternalServerError, "internal_server_error", "Không thể chuẩn bị yêu cầu upstream.")
|
| 141 |
return
|
| 142 |
}
|
| 143 |
upstreamRequestBodyBytes = openaiReqBytesMarshal
|
|
|
|
| 144 |
log.Printf("INFO: [%s] Upstream là OpenAI. Chuyển đổi và gửi yêu cầu đến %s", requestID, maskURL(upstreamURL))
|
| 145 |
}
|
| 146 |
|
| 147 |
+
// Tạo yêu cầu HTTP mới đến upstream API
|
| 148 |
req, err := http.NewRequestWithContext(c.Request.Context(), "POST", upstreamURL, bytes.NewBuffer(upstreamRequestBodyBytes))
|
| 149 |
if err != nil {
|
|
|
|
| 150 |
log.Printf("ERROR: [%s] Không thể tạo yêu cầu HTTP upstream: %v", requestID, err)
|
| 151 |
sendClaudeError(c, http.StatusInternalServerError, "internal_server_error", "Không thể tạo yêu cầu upstream.")
|
| 152 |
return
|
| 153 |
}
|
| 154 |
|
| 155 |
+
// Thiết lập các header cho yêu cầu upstream
|
| 156 |
req.Header.Set("Content-Type", "application/json")
|
| 157 |
if isStreaming {
|
| 158 |
req.Header.Set("Accept", "text/event-stream")
|
| 159 |
} else {
|
| 160 |
req.Header.Set("Accept", "application/json")
|
| 161 |
}
|
| 162 |
+
|
| 163 |
+
// Xử lý xác thực cho upstream API
|
| 164 |
+
if cfg.OpenAIAPIKey != "" { // Đảm bảo có API key để gửi
|
| 165 |
+
if cfg.UpstreamNativeClaudeFormat {
|
| 166 |
+
// Upstream là Claude native, sử dụng x-api-key và anthropic-version
|
| 167 |
+
req.Header.Set("x-api-key", cfg.OpenAIAPIKey)
|
| 168 |
+
|
| 169 |
+
anthropicVersion := os.Getenv(ANTHROPIC_VERSION_ENV_VAR)
|
| 170 |
+
if anthropicVersion == "" {
|
| 171 |
+
anthropicVersion = DEFAULT_ANTHROPIC_VERSION // Sử dụng giá trị mặc định nếu không được đặt
|
| 172 |
+
log.Printf("INFO: [%s] Biến môi trường %s không được đặt, sử dụng phiên bản Anthropic mặc định: %s", requestID, ANTHROPIC_VERSION_ENV_VAR, anthropicVersion)
|
| 173 |
+
}
|
| 174 |
+
req.Header.Set("anthropic-version", anthropicVersion)
|
| 175 |
+
log.Printf("INFO: [%s] Đặt header Anthropic-Version: %s cho upstream Claude native.", requestID, anthropicVersion)
|
| 176 |
+
|
| 177 |
+
} else {
|
| 178 |
+
// Upstream là OpenAI hoặc tương thích OpenAI, sử dụng Bearer token
|
| 179 |
+
req.Header.Set("Authorization", "Bearer "+cfg.OpenAIAPIKey)
|
| 180 |
+
}
|
| 181 |
+
} else {
|
| 182 |
+
log.Printf("WARN: [%s] OPENAI_API_KEY không được cấu hình. Yêu cầu upstream sẽ không được xác thực.", requestID)
|
| 183 |
}
|
| 184 |
|
|
|
|
| 185 |
log.Printf("INFO: [%s] Gửi yêu cầu upstream (Stream=%t) đến %s...", requestID, isStreaming, maskURL(upstreamURL))
|
| 186 |
|
| 187 |
+
// Thực hiện yêu cầu HTTP đến upstream API
|
| 188 |
httpClient := &http.Client{
|
| 189 |
+
Transport: cfg.UpstreamTransport, // Sử dụng transport đã được cấu hình (timeout, proxy, etc.)
|
| 190 |
+
Timeout: 0, // Timeout được quản lý bởi Transport
|
| 191 |
}
|
| 192 |
upstreamResp, err := httpClient.Do(req)
|
| 193 |
if err != nil {
|
|
|
|
| 194 |
log.Printf("ERROR: [%s] Yêu cầu upstream thất bại: %v", requestID, err)
|
| 195 |
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
| 196 |
sendClaudeError(c, http.StatusGatewayTimeout, "api_error", fmt.Sprintf("Gateway Timeout khi kết nối upstream (%v).", cfg.ConnectTimeout))
|
|
|
|
| 199 |
}
|
| 200 |
return
|
| 201 |
}
|
|
|
|
| 202 |
log.Printf("INFO: [%s] Nhận trạng thái upstream: %d (%s)", requestID, upstreamResp.StatusCode, http.StatusText(upstreamResp.StatusCode))
|
| 203 |
|
| 204 |
+
// Xử lý phản hồi lỗi từ upstream API
|
| 205 |
if upstreamResp.StatusCode != http.StatusOK {
|
| 206 |
errorBodyBytes, readErr := io.ReadAll(upstreamResp.Body)
|
| 207 |
if readErr != nil {
|
|
|
|
| 208 |
log.Printf("WARN: [%s] Không thể đọc nội dung lỗi upstream (Status %d): %v", requestID, upstreamResp.StatusCode, readErr)
|
| 209 |
}
|
| 210 |
+
_ = upstreamResp.Body.Close() // Luôn đóng body của phản hồi
|
| 211 |
|
| 212 |
errorBodyStr := string(errorBodyBytes)
|
|
|
|
| 213 |
log.Printf("ERROR: [%s] Upstream trả về lỗi %d. Chi tiết ngắn: %s", requestID, upstreamResp.StatusCode, truncateString(strings.TrimSpace(errorBodyStr), 100))
|
| 214 |
|
| 215 |
var errorType string
|
|
|
|
| 230 |
return
|
| 231 |
}
|
| 232 |
|
| 233 |
+
// Xử lý phản hồi thành công từ upstream API
|
| 234 |
if isStreaming {
|
| 235 |
if cfg.UpstreamNativeClaudeFormat {
|
|
|
|
| 236 |
log.Printf("INFO: [%s] Upstream Claude native stream nhận được. Bắt đầu proxy SSE (với hack usage).", requestID)
|
| 237 |
+
proxyClaudeNativeSSE(c, upstreamResp, requestID, &claudeReq)
|
| 238 |
} else {
|
|
|
|
| 239 |
log.Printf("INFO: [%s] Upstream OpenAI stream nhận được. Bắt đầu chuyển đổi SSE.", requestID)
|
| 240 |
+
modelForConversion := claudeReq.Model // Model gốc từ yêu cầu client
|
| 241 |
if openAIReqForLogging != nil && openAIReqForLogging.Model != "" {
|
| 242 |
+
modelForConversion = openAIReqForLogging.Model // Model đã được chuyển đổi (nếu có)
|
| 243 |
}
|
| 244 |
streamOpenAIResponseToClaudeSSE(c, upstreamResp, requestID, modelForConversion, &claudeReq)
|
| 245 |
}
|
| 246 |
} else { // Non-streaming
|
|
|
|
| 247 |
log.Printf("INFO: [%s] Phản hồi non-stream upstream nhận được. Đang xử lý.", requestID)
|
| 248 |
+
defer upstreamResp.Body.Close() // Đảm bảo body được đóng sau khi xử lý
|
| 249 |
|
| 250 |
respBodyBytes, errRead := io.ReadAll(upstreamResp.Body)
|
| 251 |
if errRead != nil {
|
|
|
|
| 252 |
log.Printf("ERROR: [%s] Không thể đọc nội dung phản hồi non-streaming upstream: %v", requestID, errRead)
|
| 253 |
sendClaudeError(c, http.StatusBadGateway, "api_error", "Không thể đọc phản hồi upstream.")
|
| 254 |
return
|
| 255 |
}
|
| 256 |
|
| 257 |
if cfg.UpstreamNativeClaudeFormat {
|
|
|
|
| 258 |
log.Printf("INFO: [%s] Upstream Claude native non-stream response nhận được. Chuyển tiếp trực tiếp.", requestID)
|
| 259 |
+
var tempClaudeResp map[string]interface{} // Kiểm tra xem có phải JSON hợp lệ không
|
| 260 |
if errJson := json.Unmarshal(respBodyBytes, &tempClaudeResp); errJson != nil {
|
|
|
|
| 261 |
log.Printf("ERROR: [%s] Upstream Claude native response không phải JSON hợp lệ: %v. Body: %s", requestID, errJson, string(respBodyBytes))
|
| 262 |
sendClaudeError(c, http.StatusBadGateway, "api_error", "API upstream (Claude native) trả về JSON không hợp lệ.")
|
| 263 |
return
|
| 264 |
}
|
| 265 |
clientContentType := upstreamResp.Header.Get("Content-Type")
|
| 266 |
if clientContentType == "" {
|
| 267 |
+
clientContentType = "application/json; charset=utf-8" // Mặc định nếu không có
|
| 268 |
}
|
| 269 |
+
c.Data(http.StatusOK, clientContentType, respBodyBytes) // Gửi dữ liệu thô về client
|
| 270 |
} else {
|
|
|
|
| 271 |
log.Printf("INFO: [%s] Phản hồi non-stream OpenAI upstream nhận được. Đang chuyển đổi.", requestID)
|
| 272 |
var openAIResp OpenAIResponse
|
| 273 |
if errJson := json.Unmarshal(respBodyBytes, &openAIResp); errJson != nil {
|
|
|
|
| 274 |
log.Printf("ERROR: [%s] Không thể giải mã JSON non-streaming OpenAI upstream: %v. Body: %s", requestID, errJson, string(respBodyBytes))
|
| 275 |
sendClaudeError(c, http.StatusBadGateway, "api_error", "API upstream (OpenAI) trả về JSON không hợp lệ.")
|
| 276 |
return
|
|
|
|
| 278 |
|
| 279 |
claudeRespToClient, errConv := convertOpenAIResponseToClaude(&openAIResp, requestID)
|
| 280 |
if errConv != nil {
|
|
|
|
| 281 |
log.Printf("ERROR: [%s] Không thể chuyển đổi phản hồi non-streaming OpenAI: %v", requestID, errConv)
|
| 282 |
sendClaudeError(c, http.StatusInternalServerError, "internal_server_error", fmt.Sprintf("Lỗi xử lý phản hồi upstream: %v", errConv))
|
| 283 |
return
|
| 284 |
}
|
| 285 |
c.JSON(http.StatusOK, claudeRespToClient)
|
| 286 |
}
|
|
|
|
| 287 |
log.Printf("INFO: [%s] Xử lý thành công yêu cầu non-streaming trong %v", requestID, time.Since(startTime))
|
| 288 |
}
|
| 289 |
}
|
| 290 |
|
| 291 |
+
// sendClaudeError gửi phản hồi lỗi theo định dạng Claude về client.
|
| 292 |
func sendClaudeError(c *gin.Context, statusCode int, errorType string, message string) {
|
| 293 |
errResp := ClaudeErrorResponse{
|
| 294 |
Type: "error",
|
|
|
|
| 297 |
Message: message,
|
| 298 |
},
|
| 299 |
}
|
| 300 |
+
if statusCode < 400 || statusCode > 599 { // Đảm bảo mã trạng thái HTTP hợp lệ cho lỗi
|
|
|
|
| 301 |
log.Printf("WARN: Mã trạng thái không hợp lệ %d được cung cấp cho lỗi, mặc định thành 500.", statusCode)
|
| 302 |
statusCode = http.StatusInternalServerError
|
| 303 |
}
|
| 304 |
+
c.AbortWithStatusJSON(statusCode, errResp) // Gửi lỗi và dừng xử lý tiếp theo
|
| 305 |
}
|
| 306 |
|
| 307 |
+
// truncateString rút gọn chuỗi nếu nó dài hơn maxLength.
|
| 308 |
func truncateString(s string, maxLength int) string {
|
| 309 |
if len(s) <= maxLength {
|
| 310 |
return s
|